758 lines
32 KiB
C++
758 lines
32 KiB
C++
// Copyright 2010-2025 Google LLC
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#ifndef ORTOOLS_SAT_ROUTING_CUTS_H_
|
|
#define ORTOOLS_SAT_ROUTING_CUTS_H_
|
|
|
|
#include <stdint.h>
|
|
|
|
#include <algorithm>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <ostream>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "absl/container/flat_hash_map.h"
|
|
#include "absl/log/check.h"
|
|
#include "absl/strings/str_cat.h"
|
|
#include "absl/types/span.h"
|
|
#include "ortools/base/stl_util.h"
|
|
#include "ortools/sat/cp_model.pb.h"
|
|
#include "ortools/sat/cuts.h"
|
|
#include "ortools/sat/integer.h"
|
|
#include "ortools/sat/integer_base.h"
|
|
#include "ortools/sat/linear_constraint_manager.h"
|
|
#include "ortools/sat/model.h"
|
|
#include "ortools/sat/precedences.h"
|
|
#include "ortools/sat/sat_base.h"
|
|
#include "ortools/sat/synchronization.h"
|
|
#include "ortools/sat/util.h"
|
|
|
|
namespace operations_research {
|
|
namespace sat {
|
|
|
|
// Helper to recover the mapping between nodes and "cumul" expressions in simple
|
|
// cases of route constraints (at most one expression per node and "dimension"
|
|
// -- such as time or load, and at most one relation per arc and dimension).
|
|
//
|
|
// This returns an empty vector and num_dimensions == 0 if nothing is detected.
|
|
// Otherwise it returns one expression per node and dimensions.
|
|
struct RoutingCumulExpressions {
|
|
const AffineExpression& GetNodeExpression(int node, int dimension) const {
|
|
return flat_node_dim_expressions[node * num_dimensions + dimension];
|
|
}
|
|
|
|
int num_dimensions = 0;
|
|
std::vector<AffineExpression> flat_node_dim_expressions;
|
|
};
|
|
RoutingCumulExpressions DetectDimensionsAndCumulExpressions(
|
|
int num_nodes, absl::Span<const int> tails, absl::Span<const int> heads,
|
|
absl::Span<const Literal> literals,
|
|
const ConditionalLinear2Bounds& binary_relation_repository);
|
|
|
|
// A coeff * var + offset affine expression, where `var` is always a positive
|
|
// reference (contrary to AffineExpression, where the coefficient is always
|
|
// positive).
|
|
struct NodeExpression {
|
|
IntegerVariable var;
|
|
IntegerValue coeff;
|
|
IntegerValue offset;
|
|
|
|
NodeExpression() : var(kNoIntegerVariable), coeff(0), offset(0) {}
|
|
|
|
NodeExpression(IntegerVariable var, IntegerValue coeff, IntegerValue offset)
|
|
: var(var), coeff(coeff), offset(offset) {}
|
|
|
|
explicit NodeExpression(const AffineExpression& expr) {
|
|
if (expr.var == kNoIntegerVariable || VariableIsPositive(expr.var)) {
|
|
var = expr.var;
|
|
coeff = expr.coeff;
|
|
} else {
|
|
var = PositiveVariable(expr.var);
|
|
coeff = -expr.coeff;
|
|
}
|
|
offset = expr.constant;
|
|
}
|
|
|
|
bool IsEmpty() const { return var == kNoIntegerVariable; }
|
|
|
|
IntegerValue ValueAt(IntegerValue x) const { return coeff * x + offset; }
|
|
|
|
NodeExpression Negated() const {
|
|
return NodeExpression(var, -coeff, -offset);
|
|
}
|
|
};
|
|
|
|
// Returns some bounds on y_expr - x_expr, based on the given relation and the
|
|
// given variable bounds. r.a (resp. r.b) must have the same variable as x_expr
|
|
// (resp. y_expr), which must not be kNoIntegerVariable. Moreover, the
|
|
// coefficients of these variables in x_expr, y_expr and r must not be zero.
|
|
//
|
|
// The returned bounds are generally not the best possible ones (computing them
|
|
// is equivalent to solving a MIP -- min(y_expr - x_expr) subject to r, x.var in
|
|
// x_var_bounds and y.var in y_var_bounds).
|
|
std::pair<IntegerValue, IntegerValue> GetDifferenceBounds(
|
|
const NodeExpression& x_expr, const NodeExpression& y_expr,
|
|
const sat::Relation& r,
|
|
const std::pair<IntegerValue, IntegerValue>& x_var_bounds,
|
|
const std::pair<IntegerValue, IntegerValue>& y_var_bounds);
|
|
|
|
// Helper to store the result of DetectDimensionsAndCumulExpressions() and also
|
|
// recover and store bounds on (node_expr[head] - node_expr[tail]) for each arc
|
|
// (tail -> head) assuming the arc is taken.
|
|
class RouteRelationsHelper {
|
|
public:
|
|
// Visible for testing.
|
|
RouteRelationsHelper() = default;
|
|
|
|
// Creates a RouteRelationsHelper for the given RoutesConstraint and
|
|
// associated binary relations. The vector `flat_node_dim_expressions` should
|
|
// be the result of DetectDimensionsAndCumulExpressions(). If it is empty,
|
|
// this returns nullptr.
|
|
//
|
|
// Otherwise this stores it and also computes the tightest bounds we can on
|
|
// (node_expr[head] - node_expr[tail]) for each arc, assuming the arc is taken
|
|
// (its literal is true).
|
|
static std::unique_ptr<RouteRelationsHelper> Create(
|
|
int num_nodes, absl::Span<const int> tails, absl::Span<const int> heads,
|
|
absl::Span<const Literal> literals,
|
|
absl::Span<const AffineExpression> flat_node_dim_expressions,
|
|
const ConditionalLinear2Bounds& binary_relation_repository, Model* model);
|
|
|
|
// Returns the number of "dimensions", such as time or vehicle load.
|
|
int num_dimensions() const { return num_dimensions_; }
|
|
|
|
int num_nodes() const {
|
|
return flat_node_dim_expressions_.size() / num_dimensions_;
|
|
}
|
|
|
|
int num_arcs() const {
|
|
return flat_arc_dim_relations_.size() / num_dimensions_;
|
|
}
|
|
|
|
// Returns the expression associated with the given node and dimension.
|
|
const AffineExpression& GetNodeExpression(int node, int dimension) const {
|
|
return flat_node_dim_expressions_[node * num_dimensions_ + dimension];
|
|
}
|
|
|
|
// Each arc stores the bound on expr[head] - expr[tail] \in [lhs, rhs]. Note
|
|
// that we interpret kMin/kMax integer values as not set. Such bounds will
|
|
// still be valid though because we have a precondition on the input model
|
|
// variables to be within [kMin/2, kMax/2].
|
|
struct HeadMinusTailBounds {
|
|
IntegerValue lhs = kMinIntegerValue;
|
|
IntegerValue rhs = kMaxIntegerValue;
|
|
|
|
bool operator==(const HeadMinusTailBounds& r) const {
|
|
return lhs == r.lhs && rhs == r.rhs;
|
|
}
|
|
};
|
|
const HeadMinusTailBounds& GetArcRelation(int arc, int dimension) const {
|
|
DCHECK_GE(dimension, 0);
|
|
DCHECK_LT(dimension, num_dimensions_);
|
|
DCHECK_GE(arc, 0);
|
|
return flat_arc_dim_relations_[arc * num_dimensions_ + dimension];
|
|
}
|
|
|
|
bool HasShortestPathsInformation() const {
|
|
return !flat_shortest_path_lbs_.empty();
|
|
}
|
|
|
|
// If any of the bound is at the maximum, then there is no path between tail
|
|
// and head.
|
|
bool PathExists(int tail, int head) const {
|
|
if (flat_shortest_path_lbs_.empty()) return true;
|
|
const int num_nodes_squared = num_nodes_ * num_nodes_;
|
|
for (int dim = 0; dim < num_dimensions_; ++dim) {
|
|
if (flat_shortest_path_lbs_[dim * num_nodes_squared + tail * num_nodes_ +
|
|
head] ==
|
|
std::numeric_limits<IntegerValue>::max()) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool PropagateLocalBoundsUsingShortestPaths(
|
|
const IntegerTrail& integer_trail, int tail, int head,
|
|
const absl::flat_hash_map<IntegerVariable, IntegerValue>& input,
|
|
absl::flat_hash_map<IntegerVariable, IntegerValue>* output) const {
|
|
if (flat_shortest_path_lbs_.empty()) return true;
|
|
const int num_nodes_squared = num_nodes_ * num_nodes_;
|
|
for (int dim = 0; dim < num_dimensions_; ++dim) {
|
|
const AffineExpression& tail_expr = GetNodeExpression(tail, dim);
|
|
const AffineExpression& head_expr = GetNodeExpression(head, dim);
|
|
const IntegerValue head_minus_tail_lb =
|
|
flat_shortest_path_lbs_[dim * num_nodes_squared + tail * num_nodes_ +
|
|
head];
|
|
if (head_expr.var == kNoIntegerVariable) continue;
|
|
|
|
IntegerValue tail_lb = integer_trail.LevelZeroLowerBound(tail_expr);
|
|
if (tail_expr.var != kNoIntegerVariable) {
|
|
auto it = input.find(tail_expr.var);
|
|
if (it != input.end()) {
|
|
tail_lb = std::max(tail_lb, tail_expr.ValueAt(it->second));
|
|
}
|
|
}
|
|
|
|
// head_expr >= tail_lb + head_minus_tail_lb;
|
|
const IntegerLiteral i_lit =
|
|
head_expr.GreaterOrEqual(CapAddI(tail_lb, head_minus_tail_lb));
|
|
if (i_lit.bound > integer_trail.LevelZeroUpperBound(i_lit.var)) {
|
|
return false; // not possible.
|
|
}
|
|
auto [it, inserted] = output->insert({i_lit.var, i_lit.bound});
|
|
if (!inserted) {
|
|
it->second = std::max(it->second, i_lit.bound);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Returns the level zero lower bound of the offset between the (optionally
|
|
// negated) expressions associated with the head and tail of the given arc,
|
|
// and the given dimension.
|
|
IntegerValue GetArcOffsetLowerBound(int arc, int dimension,
|
|
bool negate_expressions) const;
|
|
|
|
void RemoveArcs(absl::Span<const int> sorted_arc_indices);
|
|
|
|
private:
|
|
RouteRelationsHelper(int num_dimensions,
|
|
std::vector<AffineExpression> flat_node_dim_expressions,
|
|
std::vector<HeadMinusTailBounds> flat_arc_dim_relations,
|
|
std::vector<IntegerValue> flat_shortest_path_lbs);
|
|
|
|
void LogStats() const;
|
|
|
|
const int num_nodes_ = 0;
|
|
const int num_dimensions_ = 0;
|
|
|
|
// The expression associated with node n and dimension d is at index n *
|
|
// num_dimensions_ + d.
|
|
std::vector<AffineExpression> flat_node_dim_expressions_;
|
|
|
|
// The relation associated with arc a and dimension d is at index a *
|
|
// num_dimensions_ + d.
|
|
std::vector<HeadMinusTailBounds> flat_arc_dim_relations_;
|
|
|
|
// flat_shortest_path_lbs_[dim * num_nodes^2 + i * num_nodes + j] is a lower
|
|
// bounds on node_expression[dim][j] - node_expression[dim][i] whatever the
|
|
// path used to join node i to node j (not using the root 0). It only make
|
|
// sense for i and j != 0.
|
|
std::vector<IntegerValue> flat_shortest_path_lbs_;
|
|
};
|
|
|
|
// Computes and fills the node expressions of all the routes constraints in
|
|
// `output_model` that don't have them, if possible. The node expressions are
|
|
// inferred from the binary relations in `input_model`. Both models must have
|
|
// the same variables (they can reference the same underlying object).
|
|
// Returns the number of constraints that were filled, and the total number of
|
|
// dimensions added to them.
|
|
std::pair<int, int> MaybeFillMissingRoutesConstraintNodeExpressions(
|
|
const CpModelProto& input_model, CpModelProto& output_model);
|
|
|
|
class SpecialBinPackingHelper {
|
|
public:
|
|
SpecialBinPackingHelper() = default;
|
|
explicit SpecialBinPackingHelper(double dp_effort) : dp_effort_(dp_effort) {}
|
|
|
|
// See below.
|
|
enum ItemOrBinType {
|
|
MUST_BE_ITEM = 0, // capacity will be set at zero.
|
|
ITEM_OR_BIN = 1,
|
|
MUST_BE_BIN = 2, // demand will be set at zero.
|
|
};
|
|
|
|
// This is used to represent a "special bin packing" problem where we have
|
|
// objects that can either be items with a given demand or bins with a given
|
|
// capacity. The problem is to choose the minimum number of objects that will
|
|
// be bins, such that the other objects (items) can be packed inside.
|
|
struct ItemOrBin {
|
|
// The initial routing node that correspond to this object.
|
|
int node = 0;
|
|
|
|
// Only one option will apply, this can either be an item with given demand
|
|
// or a bin with given capacity.
|
|
//
|
|
// Important: We support negative demands and negative capacity. We just
|
|
// need that the sum of demand <= capacity for the item in that bin.
|
|
IntegerValue demand = 0;
|
|
IntegerValue capacity = 0;
|
|
|
|
// We described the problem where each object can be an item or a bin, but
|
|
// in practice we might have restriction on what object can be which, and we
|
|
// use this field to indicate that.
|
|
//
|
|
// The numerical order is important as we use that in the greedy algorithm.
|
|
// See ComputeMinNumberOfBins() code.
|
|
ItemOrBinType type = ITEM_OR_BIN;
|
|
};
|
|
|
|
// Given a "special bin packing" problem as decribed above, return a lower
|
|
// bound on the number of bins that needs to be taken.
|
|
//
|
|
// This simply sorts the object according to a greedy criteria and minimize
|
|
// the number of bins such that the "demands <= capacities" constraint is
|
|
// satisfied.
|
|
//
|
|
// If the problem is infeasible, this will return object.size() + 1, which is
|
|
// a trivially infeasible bound.
|
|
//
|
|
// TODO(user): Use fancier DP to derive tighter bound. Also, when there are
|
|
// many dimensions, the choice of which item go to which bin is correlated,
|
|
// can we exploit this?
|
|
int ComputeMinNumberOfBins(
|
|
absl::Span<ItemOrBin> objects,
|
|
std::vector<int>& objects_that_cannot_be_bin_and_reach_minimum,
|
|
std::string& info);
|
|
|
|
// Visible for testing.
|
|
//
|
|
// Returns true if we can greedily pack items with indices [num_bins, size)
|
|
// into the first num_bins bins (with indices [0, num_bins)). This function
|
|
// completely ignores the ItemOrBin types, and just uses the first num_bins
|
|
// objects as bins and the rest as items. It is up to the caller to make sure
|
|
// this is okay.
|
|
//
|
|
// This is just used to quickly assess if a more precise lower bound on the
|
|
// number of bins could gain something. It works in O(num_bins * num_items).
|
|
bool GreedyPackingWorks(int num_bins, absl::Span<const ItemOrBin> objects);
|
|
|
|
// Visible for testing.
|
|
//
|
|
// If we look at all the possible sum of item demands, it is possible that
|
|
// some value can never be reached. We use dynamic programming to compute the
|
|
// set of reachable values and tighten the capacities accordingly.
|
|
//
|
|
// Returns true iff we tightened something.
|
|
bool UseDpToTightenCapacities(absl::Span<ItemOrBin> objects);
|
|
|
|
private:
|
|
// Note that this will sort the objects so that the "best" bins are first.
|
|
// See implementation.
|
|
int ComputeMinNumberOfBinsInternal(
|
|
absl::Span<ItemOrBin> objects,
|
|
std::vector<int>& objects_that_cannot_be_bin_and_reach_minimum);
|
|
|
|
const double dp_effort_ = 1e8;
|
|
std::vector<IntegerValue> tmp_capacities_;
|
|
std::vector<int64_t> tmp_demands_;
|
|
MaxBoundedSubsetSumExact max_bounded_subset_sum_exact_;
|
|
};
|
|
|
|
inline std::ostream& operator<<(std::ostream& os,
|
|
SpecialBinPackingHelper::ItemOrBin o) {
|
|
os << absl::StrCat("d=", o.demand, " c=", o.capacity, " t=", o.type);
|
|
return os;
|
|
}
|
|
|
|
// Keep the best min outgoing/incoming flow out of a subset.
|
|
class BestBoundHelper {
|
|
public:
|
|
BestBoundHelper() = default;
|
|
explicit BestBoundHelper(std::string base_name) : base_name_(base_name) {}
|
|
|
|
void Update(int new_bound, std::string name,
|
|
absl::Span<const int> cannot_be_first = {},
|
|
absl::Span<const int> cannot_be_last = {}) {
|
|
if (new_bound < bound_) return;
|
|
if (new_bound > bound_) {
|
|
bound_ = new_bound;
|
|
name_ = name;
|
|
cannot_be_last_.clear();
|
|
cannot_be_first_.clear();
|
|
}
|
|
cannot_be_first_.insert(cannot_be_first_.begin(), cannot_be_first.begin(),
|
|
cannot_be_first.end());
|
|
cannot_be_last_.insert(cannot_be_last_.begin(), cannot_be_last.begin(),
|
|
cannot_be_last.end());
|
|
gtl::STLSortAndRemoveDuplicates(&cannot_be_first_);
|
|
gtl::STLSortAndRemoveDuplicates(&cannot_be_last_);
|
|
}
|
|
|
|
std::string name() const { return absl::StrCat(base_name_, name_); }
|
|
int bound() const { return bound_; }
|
|
|
|
// To serve the current subset with bound() num vehicles, one cannot exit
|
|
// the subset with nodes in CannotBeLast() and one cannot enter the subset
|
|
// with node in CannotBeFirst(). Moreover, one cannot enter from or leave to
|
|
// nodes in OutsideNodeThatCannotBeConnected().
|
|
absl::Span<const int> CannotBeLast() const { return cannot_be_last_; }
|
|
absl::Span<const int> CannotBeFirst() const { return cannot_be_first_; }
|
|
|
|
private:
|
|
int bound_ = 0;
|
|
std::string base_name_;
|
|
std::string name_;
|
|
std::vector<int> tmp_;
|
|
std::vector<int> cannot_be_last_;
|
|
std::vector<int> cannot_be_first_;
|
|
};
|
|
|
|
// Helper to compute the minimum flow going out of a subset of nodes, for a
|
|
// given RoutesConstraint.
|
|
class MinOutgoingFlowHelper {
|
|
public:
|
|
// Warning: The underlying tails/heads/literals might be resized from one
|
|
// call to the next of one of the functions here, so be careful.
|
|
MinOutgoingFlowHelper(int num_nodes, const std::vector<int>& tails,
|
|
const std::vector<int>& heads,
|
|
const std::vector<Literal>& literals, Model* model);
|
|
|
|
~MinOutgoingFlowHelper();
|
|
|
|
// Returns the minimum flow going out of `subset`, based on a generalization
|
|
// of the CVRP "rounded capacity inequalities", by using the given helper, if
|
|
// possible. The complexity is O((subset.size() + num_arcs()) *
|
|
// num_dimensions()).
|
|
int ComputeDimensionBasedMinOutgoingFlow(absl::Span<const int> subset,
|
|
const RouteRelationsHelper& helper,
|
|
BestBoundHelper* best_bound);
|
|
|
|
// Returns the minimum flow going out of `subset`, based on a conservative
|
|
// estimate of the maximum number of nodes of a feasible path inside this
|
|
// subset. `subset` must not be empty and must not contain the depot (node 0).
|
|
// Paths are approximated by their length and their last node, and can thus
|
|
// contain cycles. The complexity is O(subset.size() ^ 3).
|
|
int ComputeMinOutgoingFlow(absl::Span<const int> subset);
|
|
|
|
// Same as above, but uses less conservative estimates (paths are approximated
|
|
// by their set of nodes and their last node -- hence they can't contain
|
|
// cycles). The complexity is O(2 ^ subset.size()).
|
|
int ComputeTightMinOutgoingFlow(absl::Span<const int> subset);
|
|
|
|
// Returns false if the given subset CANNOT be served by k routes.
|
|
// Returns true if we have a route or we don't know for sure (if we abort).
|
|
// The parameter k must be positive.
|
|
//
|
|
// Even more costly algo in O(n!/k!*k^(n-k)) that answers the question exactly
|
|
// given the available enforced linear1 and linear2 constraints. However it
|
|
// can stop as soon as one solution is found.
|
|
//
|
|
// If special_node is non-negative, we will only look for routes that
|
|
// - Start at this special_node if use_forward_direction = true
|
|
// - End at this special_node if use_forward_direction = false
|
|
//
|
|
// If the RouteRelationsHelper is non null, then we will use "shortest path"
|
|
// bounds instead of recomputing them from the binary relation of the model.
|
|
//
|
|
// TODO(user): the complexity also depends on the longest route and improves
|
|
// if routes fail quickly. Give a better estimate?
|
|
bool SubsetMightBeServedWithKRoutes(
|
|
int k, absl::Span<const int> subset,
|
|
RouteRelationsHelper* helper = nullptr,
|
|
LinearConstraintManager* manager = nullptr, int special_node = -1,
|
|
bool use_forward_direction = true);
|
|
|
|
// Advanced. If non-empty, and one of the functions above proved that a subset
|
|
// needs at least k vehicles to serve it, then these vector list the nodes
|
|
// that cannot be first (resp. last) in one of the solution with k routes. If
|
|
// a node is listed here, it means we will need at least k + 1 routes to serve
|
|
// the subset and enter (resp. leave) from that node.
|
|
//
|
|
// This can be used to either reduce the size of the subset and still need
|
|
// k vehicles, or generate stronger cuts.
|
|
absl::Span<const int> CannotBeLast() const { return cannot_be_last_; };
|
|
absl::Span<const int> CannotBeFirst() const { return cannot_be_first_; };
|
|
|
|
// Just for stats reporting.
|
|
void ReportDpSkip() { num_full_dp_skips_++; }
|
|
|
|
private:
|
|
void PrecomputeDataForSubset(absl::Span<const int> subset);
|
|
|
|
// Given a subset S to serve in a route constraint, returns a special bin
|
|
// packing problem (defined above) where the minimum number of bins will
|
|
// correspond to the minimum number of vehicles needed to serve this subset.
|
|
//
|
|
// One way to derive such reduction is as follow.
|
|
//
|
|
// If we look at a path going through the subset, it will touch in order the
|
|
// nodes P = {n_0, ..., n_e}. It will enter S at a "start" node n_0 and leave
|
|
// at a "end" node n_e.
|
|
//
|
|
// We assume (see the RouteRelationsHelper) that each node n has an
|
|
// associated variable X_n, and that each arc t->h has an associated relation
|
|
// lhs(t,h) <= X_h - X_t. Summing all these inequalities along the path above
|
|
// we get:
|
|
// Sum_{i \in [1..e]} lhs(n_i, n_(i+1)) <= X_(n_e) - X_(n_0)
|
|
// introducing:
|
|
// - d(n) = min_(i \in S) lhs(i, n) [minimum incoming weight in subset S]
|
|
// - UB = max_(i \in S) upper_bound(X_i)
|
|
// We get:
|
|
// Sum_{n \in P \ n_0} d(n) <= UB - lower_bound(n_0)
|
|
//
|
|
// Here we can see that the "starting node" n0 is on the "capacity" side and
|
|
// will serve the role of a bin with capacity (UB - lower_bound(n_0)), whereas
|
|
// the other nodes n will be seen as "item" with demands d(i).
|
|
//
|
|
// Given that the set of paths going through S must be disjoint and serve all
|
|
// the nodes, we get exactly the special bin packing problem described above
|
|
// where the starting nodes are the bins and the other inner-nodes are the
|
|
// items.
|
|
//
|
|
// Note that if a node has no incoming arc from within S, it must be a start
|
|
// (i.e. a bin). And if a node has no incoming arcs from outside S, it cannot
|
|
// be a start an must be an inner node (i.e. an item). We can exploit this to
|
|
// derive better bounds.
|
|
//
|
|
// We just explained the reduction using incoming arcs and starts of route,
|
|
// but we can do the same with outgoing arcs and ends of route. We can also
|
|
// change the dimension (the X_i) and variable direction used in the
|
|
// RouteRelationsHelper to exploit relations X_h - X_t <= rhs(t,h) instead.
|
|
//
|
|
// We provide a reduction for the cross product of:
|
|
// - Each possible dimension in the RouteRelationsHelper.
|
|
// - lhs or rhs (when negate_expressions = true) in X - Y \in [lhs, rhs].
|
|
// - (start and incoming arcs) or (ends and outgoing arcs).
|
|
//
|
|
// Warning: the returned Span<> is only valid until the next call to this
|
|
// function.
|
|
//
|
|
// TODO(user): Given the info for a subset, we can derive bounds for any
|
|
// smaller set included in it. We just have to ignore the MUST_BE_ITEM
|
|
// type as this might no longer be true. That might be interesting.
|
|
absl::Span<SpecialBinPackingHelper::ItemOrBin>
|
|
RelaxIntoSpecialBinPackingProblem(absl::Span<const int> subset, int dimension,
|
|
bool negate_expressions, bool use_incoming,
|
|
const RouteRelationsHelper& helper);
|
|
|
|
// Returns the minimum flow going out of a subset of size `subset_size`,
|
|
// assuming that the longest feasible path inside this subset has
|
|
// `longest_path_length` nodes and that there are at most `max_longest_paths`
|
|
// such paths.
|
|
int GetMinOutgoingFlow(int subset_size, int longest_path_length,
|
|
int max_longest_paths);
|
|
|
|
const std::vector<int>& tails_;
|
|
const std::vector<int>& heads_;
|
|
const std::vector<Literal>& literals_;
|
|
const ConditionalLinear2Bounds& binary_relation_repository_;
|
|
const ImpliedBounds& implied_bounds_;
|
|
const Trail& trail_;
|
|
const IntegerTrail& integer_trail_;
|
|
const IntegerEncoder& integer_encoder_;
|
|
const RootLevelLinear2Bounds& root_level_bounds_;
|
|
SharedStatistics* shared_stats_;
|
|
|
|
// Temporary data used by ComputeMinOutgoingFlow(). Always contain default
|
|
// values, except while ComputeMinOutgoingFlow() is running.
|
|
// ComputeMinOutgoingFlow() computes, for each i in [0, |subset|), whether
|
|
// each node n in the subset could appear at position i in a feasible path.
|
|
// It computes whether n can appear at position i based on which nodes can
|
|
// appear at position i-1, and based on the arc literals and some linear
|
|
// constraints they enforce. To save memory, it only stores data about two
|
|
// consecutive positions at a time: a "current" position i, and a "next"
|
|
// position i+1.
|
|
|
|
std::vector<int> tmp_subset_;
|
|
std::vector<bool> in_subset_;
|
|
std::vector<int> index_in_subset_;
|
|
|
|
// For each node n, the indices (in tails_, heads_) of the m->n and n->m arcs
|
|
// inside the subset (self arcs excepted).
|
|
std::vector<std::vector<int>> incoming_arc_indices_;
|
|
std::vector<std::vector<int>> outgoing_arc_indices_;
|
|
|
|
// This can only be true for node in the current subset. If a node 'n' has no
|
|
// incoming arcs from outside the subset, the part of a route serving node 'n'
|
|
// in a subset cannot start at that node. And if it has no outoing arc leaving
|
|
// the subset, it cannot end at that node. This can be used to derive tighter
|
|
// bounds.
|
|
std::vector<bool> has_incoming_arcs_from_outside_;
|
|
std::vector<bool> has_outgoing_arcs_to_outside_;
|
|
|
|
// If a subset has an unique arc arriving or leaving at a given node, we can
|
|
// derive tighter bounds.
|
|
class UniqueArc {
|
|
public:
|
|
bool IsUnique() const { return num_seen_ == 1; }
|
|
int Get() const { return arc_; }
|
|
|
|
void Add(int arc) {
|
|
++num_seen_;
|
|
arc_ = arc;
|
|
}
|
|
|
|
private:
|
|
int num_seen_ = 0;
|
|
int arc_ = 0;
|
|
};
|
|
std::vector<UniqueArc> incoming_arcs_from_outside_;
|
|
std::vector<UniqueArc> outgoing_arcs_to_outside_;
|
|
|
|
// For each node n, whether it can appear at the current and next position in
|
|
// a feasible path.
|
|
std::vector<bool> reachable_;
|
|
std::vector<bool> next_reachable_;
|
|
// For each node n, the lower bound of each variable (appearing in a linear
|
|
// constraint enforced by some incoming arc literal), if n appears at the
|
|
// current and next position in a feasible path. Variables not appearing in
|
|
// these maps have no tighter lower bound that the one from the IntegerTrail
|
|
// (at decision level 0).
|
|
std::vector<absl::flat_hash_map<IntegerVariable, IntegerValue>>
|
|
node_var_lower_bounds_;
|
|
std::vector<absl::flat_hash_map<IntegerVariable, IntegerValue>>
|
|
next_node_var_lower_bounds_;
|
|
|
|
SpecialBinPackingHelper bin_packing_helper_;
|
|
std::vector<SpecialBinPackingHelper::ItemOrBin> tmp_bin_packing_problem_;
|
|
|
|
std::vector<int> objects_that_cannot_be_bin_and_reach_minimum_;
|
|
|
|
std::vector<int> cannot_be_last_;
|
|
std::vector<int> cannot_be_first_;
|
|
|
|
// Statistics.
|
|
int64_t num_full_dp_skips_ = 0;
|
|
int64_t num_full_dp_calls_ = 0;
|
|
int64_t num_full_dp_early_abort_ = 0;
|
|
int64_t num_full_dp_work_abort_ = 0;
|
|
int64_t num_full_dp_rc_skip_ = 0;
|
|
int64_t num_full_dp_unique_arc_ = 0;
|
|
absl::flat_hash_map<std::string, int> num_by_type_;
|
|
};
|
|
|
|
// Given a graph with nodes in [0, num_nodes) and a set of arcs (the order is
|
|
// important), this will:
|
|
// - Start with each nodes in separate "subsets".
|
|
// - Consider the arc in order, and each time one connects two separate
|
|
// subsets, merge the two subsets into a new one.
|
|
// - Stops when there is only 'stop_at_num_components' subset left.
|
|
// - Output all subsets generated this way (at most 2 * num_nodes). The
|
|
// subsets spans will point in the subset_data vector (which will be of size
|
|
// exactly num_nodes).
|
|
//
|
|
// This is an heuristic to generate interesting cuts for TSP or other graph
|
|
// based constraints. We roughly follow the algorithm described in section 6 of
|
|
// "The Traveling Salesman Problem, A computational Study", David L. Applegate,
|
|
// Robert E. Bixby, Vasek Chvatal, William J. Cook.
|
|
//
|
|
// Note that this is mainly a "symmetric" case algo, but it does still work for
|
|
// the asymmetric case.
|
|
//
|
|
// TODO(user): Returns the tree instead and let caller call
|
|
// ExtractAllSubsetsFromForest().
|
|
void GenerateInterestingSubsets(int num_nodes,
|
|
absl::Span<const std::pair<int, int>> arcs,
|
|
int stop_at_num_components,
|
|
std::vector<int>* subset_data,
|
|
std::vector<absl::Span<const int>>* subsets);
|
|
|
|
// Given a set of rooted tree on n nodes represented by the parent vector,
|
|
// returns the n sets of nodes corresponding to all the possible subtree. Note
|
|
// that the output memory is just n as all subset will point into the same
|
|
// vector.
|
|
//
|
|
// This assumes no cycles, otherwise it will not crash but the result will not
|
|
// be correct.
|
|
//
|
|
// In the TSP context, if the tree is a Gomory-Hu cut tree, this will returns
|
|
// a set of "min-cut" that contains a min-cut for all node pairs.
|
|
//
|
|
// TODO(user): This also allocate O(n) memory internally, we could reuse it from
|
|
// call to call if needed.
|
|
void ExtractAllSubsetsFromForest(
|
|
absl::Span<const int> parent, std::vector<int>* subset_data,
|
|
std::vector<absl::Span<const int>>* subsets,
|
|
int node_limit = std::numeric_limits<int>::max());
|
|
|
|
// In the routing context, we usually always have lp_value in [0, 1] and only
|
|
// looks at arcs with a lp_value that is not too close to zero.
|
|
struct ArcWithLpValue {
|
|
int tail;
|
|
int head;
|
|
double lp_value;
|
|
|
|
bool operator==(const ArcWithLpValue& o) const {
|
|
return tail == o.tail && head == o.head && lp_value == o.lp_value;
|
|
}
|
|
};
|
|
|
|
// Regroups and sum the lp values on duplicate arcs or reversed arcs
|
|
// (tail->head) and (head->tail). As a side effect, we will always
|
|
// have tail <= head.
|
|
void SymmetrizeArcs(std::vector<ArcWithLpValue>* arcs);
|
|
|
|
// Given a set of arcs on a directed graph with n nodes (in [0, num_nodes)),
|
|
// returns a "parent" vector of size n encoding a rooted Gomory-Hu tree.
|
|
//
|
|
// Note that usually each edge in the tree is attached a max-flow value (its
|
|
// weight), but we don't need it here. It can be added if needed. This tree as
|
|
// the property that for all (s, t) pair of nodes, if you take the minimum
|
|
// weight edge on the path from s to t and split the tree in two, then this is a
|
|
// min-cut for that pair.
|
|
//
|
|
// IMPORTANT: This algorithm currently "symmetrize" the graph, so we will
|
|
// actually have all the min-cuts that minimize sum incoming + sum outgoing lp
|
|
// values. The algo do not work as is on an asymmetric graph. Note however that
|
|
// because of flow conservation, our outgoing lp values should be the same as
|
|
// our incoming one on a circuit/route constraint.
|
|
//
|
|
// We use a simple implementation described in "Very Simple Methods for All
|
|
// Pairs Network Flow Analysis", Dan Gusfield, 1990,
|
|
// https://ranger.uta.edu/~weems/NOTES5311/LAB/LAB2SPR21/gusfield.huGomory.pdf
|
|
std::vector<int> ComputeGomoryHuTree(
|
|
int num_nodes, absl::Span<const ArcWithLpValue> relevant_arcs);
|
|
|
|
// Cut generator for the circuit constraint, where in any feasible solution, the
|
|
// arcs that are present (variable at 1) must form a circuit through all the
|
|
// nodes of the graph. Self arc are forbidden in this case.
|
|
//
|
|
// In more generality, this currently enforce the resulting graph to be strongly
|
|
// connected. Note that we already assume basic constraint to be in the lp, so
|
|
// we do not add any cuts for components of size 1.
|
|
CutGenerator CreateStronglyConnectedGraphCutGenerator(
|
|
int num_nodes, absl::Span<const int> tails, absl::Span<const int> heads,
|
|
absl::Span<const Literal> literals, Model* model);
|
|
|
|
// Almost the same as CreateStronglyConnectedGraphCutGenerator() but for each
|
|
// components, computes the demand needed to serves it, and depending on whether
|
|
// it contains the depot (node zero) or not, compute the minimum number of
|
|
// vehicle that needs to cross the component border.
|
|
// `flat_node_dim_expressions` must have num_dimensions (possibly 0) times
|
|
// num_nodes elements, with the expression associated with node n and dimension
|
|
// d at index n * num_dimensions + d.
|
|
CutGenerator CreateCVRPCutGenerator(
|
|
int num_nodes, absl::Span<const int> tails, absl::Span<const int> heads,
|
|
absl::Span<const Literal> literals,
|
|
absl::Span<const AffineExpression> flat_node_dim_expressions, Model* model);
|
|
|
|
// Try to find a subset where the current LP capacity of the outgoing or
|
|
// incoming arc is not enough to satisfy the demands.
|
|
//
|
|
// We support the special value -1 for tail or head that means that the arc
|
|
// comes from (or is going to) outside the nodes in [0, num_nodes). Such arc
|
|
// must still have a capacity assigned to it.
|
|
//
|
|
// TODO(user): Support general linear expression for capacities.
|
|
// TODO(user): Some model applies the same capacity to both an arc and its
|
|
// reverse. Also support this case.
|
|
CutGenerator CreateFlowCutGenerator(
|
|
int num_nodes, const std::vector<int>& tails, const std::vector<int>& heads,
|
|
const std::vector<AffineExpression>& arc_capacities,
|
|
std::function<void(const std::vector<bool>& in_subset,
|
|
IntegerValue* min_incoming_flow,
|
|
IntegerValue* min_outgoing_flow)>
|
|
get_flows,
|
|
Model* model);
|
|
|
|
} // namespace sat
|
|
} // namespace operations_research
|
|
|
|
#endif // ORTOOLS_SAT_ROUTING_CUTS_H_
|