415 lines
15 KiB
C++
415 lines
15 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_PRESOLVE_UTIL_H_
|
|
#define ORTOOLS_SAT_PRESOLVE_UTIL_H_
|
|
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "absl/container/flat_hash_map.h"
|
|
#include "absl/container/flat_hash_set.h"
|
|
#include "absl/random/bit_gen_ref.h"
|
|
#include "absl/types/span.h"
|
|
#include "ortools/base/strong_vector.h"
|
|
#include "ortools/base/timer.h"
|
|
#include "ortools/sat/cp_model.pb.h"
|
|
#include "ortools/sat/cp_model_utils.h"
|
|
#include "ortools/sat/util.h"
|
|
#include "ortools/util/bitset.h"
|
|
#include "ortools/util/logging.h"
|
|
#include "ortools/util/sorted_interval_list.h"
|
|
#include "ortools/util/strong_integers.h"
|
|
#include "ortools/util/time_limit.h"
|
|
|
|
namespace operations_research {
|
|
namespace sat {
|
|
|
|
// Simple helper class to:
|
|
// - log in an uniform way a "time-consuming" presolve operation.
|
|
// - track a deterministic work limit.
|
|
// - update the deterministic time on finish.
|
|
class PresolveTimer {
|
|
public:
|
|
PresolveTimer(std::string name, SolverLogger* logger, TimeLimit* time_limit)
|
|
: name_(std::move(name)), logger_(logger), time_limit_(time_limit) {
|
|
timer_.Start();
|
|
}
|
|
|
|
// Track the work done (which is also the deterministic time).
|
|
// By default we want a limit of around 1 deterministic seconds.
|
|
void AddToWork(double dtime) { work_ += dtime; }
|
|
void TrackSimpleLoop(int size) { work_ += 5e-9 * size; }
|
|
bool WorkLimitIsReached() const { return work_ >= 1.0; }
|
|
|
|
// Extra stats=value to display at the end.
|
|
// We filter value of zero to have less clutter.
|
|
void AddCounter(std::string name, int64_t count) {
|
|
if (count == 0) return;
|
|
counters_.emplace_back(std::move(name), count);
|
|
}
|
|
|
|
// Extra info at the end of the log line.
|
|
void AddMessage(std::string name) { extra_infos_.push_back(std::move(name)); }
|
|
|
|
// Update dtime and log operation summary.
|
|
~PresolveTimer();
|
|
|
|
private:
|
|
const std::string name_;
|
|
|
|
WallTimer timer_;
|
|
SolverLogger* logger_;
|
|
TimeLimit* time_limit_;
|
|
|
|
double work_ = 0;
|
|
std::vector<std::pair<std::string, int64_t>> counters_;
|
|
std::vector<std::string> extra_infos_;
|
|
};
|
|
|
|
// If for each literal of a clause, we can infer a domain on an integer
|
|
// variable, then we know that this variable domain is included in the union of
|
|
// such inferred domains.
|
|
//
|
|
// This allows to propagate "element" like constraints encoded as enforced
|
|
// linear relations, and other more general reasoning.
|
|
//
|
|
// TODO(user): Also use these "deductions" in the solver directly. This is done
|
|
// in good MIP solvers, and we should exploit them more.
|
|
//
|
|
// TODO(user): Also propagate implicit clauses (lit, not(lit)). Maybe merge
|
|
// that with probing code? it might be costly to store all deduction done by
|
|
// probing though, but I think this is what MIP solver do.
|
|
class DomainDeductions {
|
|
public:
|
|
// Adds the fact that enforcement => var \in domain.
|
|
//
|
|
// Important: No need to store any deductions where the domain is a superset
|
|
// of the current variable domain.
|
|
void AddDeduction(int literal_ref, int var, Domain domain);
|
|
|
|
// Returns the domain of var when literal_ref is true.
|
|
// If there is no information, returns Domain::AllValues().
|
|
Domain ImpliedDomain(int literal_ref, int var) const;
|
|
|
|
// Returns list of (var, domain) that were deduced because:
|
|
// 1/ We have a domain deduction for var and all literal from the clause
|
|
// 2/ So we can take the union of all the deduced domains.
|
|
//
|
|
// TODO(user): We could probably be even more efficient. We could also
|
|
// compute exactly what clauses need to be "waked up" as new deductions are
|
|
// added.
|
|
std::vector<std::pair<int, Domain>> ProcessClause(
|
|
absl::Span<const int> clause);
|
|
|
|
// Optimization. Any following ProcessClause() will be fast if no more
|
|
// deduction touching that clause are added.
|
|
void MarkProcessingAsDoneForNow() {
|
|
something_changed_.ClearAndResize(something_changed_.size());
|
|
}
|
|
|
|
// Returns the total number of "deductions" stored by this class.
|
|
int NumDeductions() const { return deductions_.size(); }
|
|
|
|
private:
|
|
DEFINE_STRONG_INDEX_TYPE(Index);
|
|
Index IndexFromLiteral(int ref) const {
|
|
return Index(ref >= 0 ? 2 * ref : -2 * ref - 1);
|
|
}
|
|
|
|
std::vector<int> tmp_num_occurrences_;
|
|
|
|
SparseBitset<Index> something_changed_;
|
|
util_intops::StrongVector<Index, std::vector<int>> enforcement_to_vars_;
|
|
absl::flat_hash_map<std::pair<Index, int>, Domain> deductions_;
|
|
};
|
|
|
|
// Does "to_modify += factor * to_add". Both constraint must be linear.
|
|
// Returns false and does not change anything in case of overflow.
|
|
//
|
|
// Note that the enforcement literals (if any) are ignored and left untouched.
|
|
bool AddLinearConstraintMultiple(int64_t factor, const ConstraintProto& to_add,
|
|
ConstraintProto* to_modify);
|
|
|
|
// Replaces the variable var in ct using the definition constraint.
|
|
// Currently the coefficient in the definition must be 1 or -1.
|
|
//
|
|
// This might return false and NOT modify ConstraintProto in case of overflow
|
|
// or other issue with the substitution.
|
|
bool SubstituteVariable(int var, int64_t var_coeff_in_definition,
|
|
const ConstraintProto& definition, ConstraintProto* ct);
|
|
|
|
// Same as a vector<T> or hash_map<int, T> where the index are in [0, size),
|
|
// but optimized for the case where only a few entries are touched before the
|
|
// vector need to be reset to zero and used again.
|
|
//
|
|
// TODO(user): Maybe a SparseBitset + sparse clear is better. But this is a
|
|
// worth alternative to test IMO.
|
|
template <typename T>
|
|
class VectorWithSparseUsage {
|
|
public:
|
|
// Taking a view allow to cache the never changing addresses.
|
|
class View {
|
|
public:
|
|
View(int* i, int* pi, T* pv)
|
|
: index_to_position_(i),
|
|
position_to_index_(pi),
|
|
position_to_value_(pv) {}
|
|
|
|
T& operator[](int index) {
|
|
const int p = index_to_position_[index];
|
|
if (p < size_ && index == position_to_index_[p]) {
|
|
// [index] was already called.
|
|
return position_to_value_[p];
|
|
}
|
|
|
|
// First call.
|
|
index_to_position_[index] = size_;
|
|
position_to_index_[size_] = index;
|
|
position_to_value_[size_] = 0;
|
|
return position_to_value_[size_++];
|
|
}
|
|
|
|
private:
|
|
int size_ = 0;
|
|
int* const index_to_position_;
|
|
int* const position_to_index_;
|
|
T* const position_to_value_;
|
|
};
|
|
|
|
// This reserve the size for using indices in [0, size).
|
|
View ClearedView(int size) {
|
|
index_to_position_.resize(size);
|
|
position_to_index_.resize(size);
|
|
position_to_value_.resize(size);
|
|
return View(index_to_position_.data(), position_to_index_.data(),
|
|
position_to_value_.data());
|
|
}
|
|
|
|
private:
|
|
// We never need to clear this. We can detect stale positions if
|
|
// position_to_index_[index_to_position_[index]] is inconsistent.
|
|
std::vector<int> index_to_position_;
|
|
|
|
// Only the beginning [0, num touched indices) is used here.
|
|
std::vector<int> position_to_index_;
|
|
std::vector<T> position_to_value_;
|
|
};
|
|
|
|
// Try to get more precise min/max activity of a linear constraints using
|
|
// at most ones from the model. This is heuristic based but should be relatively
|
|
// fast.
|
|
//
|
|
// TODO(user): Use better algorithm. The problem is the same as finding upper
|
|
// bound to the classic problem: maximum-independent set in a graph. We also
|
|
// only use at most ones, but we could use the more general binary implication
|
|
// graph.
|
|
class ActivityBoundHelper {
|
|
public:
|
|
ActivityBoundHelper() = default;
|
|
|
|
// The at most one constraint must be added before linear constraint are
|
|
// processed. The functions below will still works, but do nothing more than
|
|
// compute trivial bounds.
|
|
void ClearAtMostOnes();
|
|
void AddAtMostOne(absl::Span<const int> amo);
|
|
void AddAllAtMostOnes(const CpModelProto& proto);
|
|
|
|
// Computes the max/min activity of a linear expression involving only
|
|
// Booleans.
|
|
//
|
|
// Accepts a list of (literal, coefficient). Note that all literal will be
|
|
// interpreted as referring to [0, 1] variables. We use the CpModelProto
|
|
// convention for negated literal index.
|
|
//
|
|
// If conditional is not nullptr, then conditional[i][0/1] will give the
|
|
// max activity if the literal at position i is false/true. This can be used
|
|
// to fix variables or extract enforcement literal.
|
|
//
|
|
// Important: We shouldn't have duplicates or a lit and NegatedRef(lit)
|
|
// appearing both.
|
|
//
|
|
// Note: the result of this function is not exact (it uses an heuristic to
|
|
// detect AMOs), but it does not depend on the order of the input terms, so
|
|
// passing an input in non-deterministic order is fine.
|
|
//
|
|
// TODO(user): Indicate when the bounds are trivial (i.e. not intersection
|
|
// with any amo) so that we don't waste more time processing the result?
|
|
int64_t ComputeMaxActivity(
|
|
absl::Span<const std::pair<int, int64_t>> terms,
|
|
std::vector<std::array<int64_t, 2>>* conditional = nullptr) {
|
|
return ComputeActivity(/*compute_min=*/false, terms, conditional);
|
|
}
|
|
int64_t ComputeMinActivity(
|
|
absl::Span<const std::pair<int, int64_t>> terms,
|
|
std::vector<std::array<int64_t, 2>>* conditional = nullptr) {
|
|
return ComputeActivity(/*compute_min=*/true, terms, conditional);
|
|
}
|
|
|
|
// Computes relevant info to presolve a constraint with enforcement using
|
|
// at most one information.
|
|
//
|
|
// This returns false iff the enforcement list cannot be satisfied.
|
|
// It filters the enforcement list if some are consequences of other.
|
|
// It fills the given set with the literal that must be true due to the
|
|
// enforcement. Note that only literals or negated literal appearing in ref
|
|
// are filled.
|
|
bool PresolveEnforcement(absl::Span<const int> refs, ConstraintProto* ct,
|
|
absl::flat_hash_set<int>* literals_at_true);
|
|
|
|
// For each enforcement literal enf, if not(enf) implies that the constraint
|
|
// is trivial, then we can just remove not(enf) from the list.
|
|
//
|
|
// Actually, we could even "lift" such enforcement so that if it is negative
|
|
// the constraint is still trivial but tighter.
|
|
int RemoveEnforcementThatMakesConstraintTrivial(
|
|
absl::Span<const std::pair<int, int64_t>> boolean_terms,
|
|
const Domain& other_terms, const Domain& rhs, ConstraintProto* ct);
|
|
|
|
// Partition the list of literals into disjoint at most ones. The returned
|
|
// spans are only valid until another function from this class is used.
|
|
std::vector<absl::Span<const int>> PartitionLiteralsIntoAmo(
|
|
absl::Span<const int> literals);
|
|
|
|
// Returns true iff the given literal are in at most one relationship.
|
|
bool IsAmo(absl::Span<const int> literals);
|
|
|
|
// Returns in how many amo var or Not(var) are part of.
|
|
int NumAmoForVariable(int var) const {
|
|
const Index i = IndexFromLiteral(var);
|
|
const Index j = IndexFromLiteral(NegatedRef(var));
|
|
return (i < amo_indices_.size() ? amo_indices_[i].size() : 0) +
|
|
(j < amo_indices_.size() ? amo_indices_[j].size() : 0);
|
|
}
|
|
|
|
private:
|
|
DEFINE_STRONG_INDEX_TYPE(Index);
|
|
Index IndexFromLiteral(int ref) const {
|
|
return Index(ref >= 0 ? 2 * ref : -2 * ref - 1);
|
|
}
|
|
|
|
bool AppearInTriggeredAmo(int literal);
|
|
|
|
int64_t ComputeActivity(
|
|
bool compute_min, absl::Span<const std::pair<int, int64_t>> terms,
|
|
std::vector<std::array<int64_t, 2>>* conditional = nullptr);
|
|
|
|
void PartitionIntoAmo(absl::Span<const std::pair<int, int64_t>> terms);
|
|
|
|
// All coeff must be >= 0 here. Note that in practice, we shouldn't have
|
|
// zero coeff, but we still support it.
|
|
int64_t ComputeMaxActivityInternal(
|
|
absl::Span<const std::pair<int, int64_t>> terms,
|
|
std::vector<std::array<int64_t, 2>>* conditional = nullptr);
|
|
|
|
// We use an unique index by at most one, and just stores for each literal
|
|
// the at most one to which it belong.
|
|
int num_at_most_ones_ = 0;
|
|
util_intops::StrongVector<Index, std::vector<int>> amo_indices_;
|
|
|
|
std::vector<std::pair<int, int64_t>> tmp_terms_for_compute_activity_;
|
|
|
|
struct TermWithIndex {
|
|
int64_t coeff;
|
|
Index index;
|
|
int span_index;
|
|
};
|
|
std::vector<TermWithIndex> to_sort_;
|
|
|
|
// We partition the set of term into disjoint at most one.
|
|
VectorWithSparseUsage<int64_t> amo_sums_;
|
|
std::vector<int> partition_;
|
|
std::vector<int64_t> max_by_partition_;
|
|
std::vector<int64_t> second_max_by_partition_;
|
|
|
|
// Used by PartitionLiteralsIntoAmo().
|
|
CompactVectorVector<int, int> part_to_literals_;
|
|
|
|
absl::flat_hash_set<int> triggered_amo_;
|
|
absl::flat_hash_set<int> tmp_set_;
|
|
};
|
|
|
|
// Class to help detects clauses that differ on a single literal.
|
|
class ClauseWithOneMissingHasher {
|
|
public:
|
|
explicit ClauseWithOneMissingHasher(absl::BitGenRef random)
|
|
: random_(random) {}
|
|
|
|
// We use the proto encoding of literals here.
|
|
void RegisterClause(int c, absl::Span<const int> clause);
|
|
|
|
// Returns a hash of the clause with index c and literal ref removed.
|
|
// This assumes that ref was part of the clause. Work in O(1).
|
|
uint64_t HashWithout(int c, int ref) const {
|
|
return clause_to_hash_[c] ^ literal_to_hash_[IndexFromLiteral(ref)];
|
|
}
|
|
|
|
// Returns a hash of the negation of all the given literals.
|
|
uint64_t HashOfNegatedLiterals(absl::Span<const int> literals);
|
|
|
|
private:
|
|
DEFINE_STRONG_INDEX_TYPE(Index);
|
|
Index IndexFromLiteral(int ref) const {
|
|
return Index(ref >= 0 ? 2 * ref : -2 * ref - 1);
|
|
}
|
|
|
|
absl::BitGenRef random_;
|
|
util_intops::StrongVector<Index, uint64_t> literal_to_hash_;
|
|
std::vector<uint64_t> clause_to_hash_;
|
|
};
|
|
|
|
// Specific function. Returns true if the negation of all literals in clause
|
|
// except literal is exactly equal to the literal of enforcement.
|
|
//
|
|
// We assumes that enforcement and negated(clause) are sorted lexicographically
|
|
// Or negated(enforcement) and clause. Both option works. If not, we will only
|
|
// return false more often. When we return true, the property is enforced.
|
|
//
|
|
// TODO(user): For the same complexity, we do not need to specify literal and
|
|
// can recover it.
|
|
inline bool ClauseIsEnforcementImpliesLiteral(absl::Span<const int> clause,
|
|
absl::Span<const int> enforcement,
|
|
int literal) {
|
|
if (clause.size() != enforcement.size() + 1) return false;
|
|
int j = 0;
|
|
for (int i = 0; i < clause.size(); ++i) {
|
|
if (clause[i] == literal) continue;
|
|
if (clause[i] != NegatedRef(enforcement[j])) return false;
|
|
++j;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Same as LinearsDifferAtOneTerm() below but also fills the differing terms.
|
|
bool FindSingleLinearDifference(const LinearConstraintProto& lin1,
|
|
const LinearConstraintProto& lin2, int* var1,
|
|
int64_t* coeff1, int* var2, int64_t* coeff2);
|
|
|
|
// Returns true iff the two linear constraint only differ at a single term.
|
|
//
|
|
// Preconditions: Constraint should be sorted by variable and of same size.
|
|
inline bool LinearsDifferAtOneTerm(const LinearConstraintProto& lin1,
|
|
const LinearConstraintProto& lin2) {
|
|
int var1, var2;
|
|
int64_t coeff1, coeff2;
|
|
return FindSingleLinearDifference(lin1, lin2, &var1, &coeff1, &var2, &coeff2);
|
|
}
|
|
|
|
} // namespace sat
|
|
} // namespace operations_research
|
|
|
|
#endif // ORTOOLS_SAT_PRESOLVE_UTIL_H_
|