ortools cp_model_presolve cp_model_postsolve model_solver用法功能源码分析
文章目录
cp_model_presolve.h
功能
CpModelPresolver是一个CP模型的预处理器,用于优化和简化模型。 它实施了一系列的转换和简化技术,以减少模型的复杂性,并且提高求解效率。
CpModelPresolver的主要功能包括:
- 删除无用变量和约束:删除没有任何影响的变量和约束,以减少模型的大小和求解时间。
- 合并等价约束:合并具有相同约束条件的约束,以减少模型的复杂度。
- 约束改写:将复杂的约束改写为更简单的形式,以提高求解效率。
- 变量替换:将一组变量替换为新的变量,以简化模型并减少搜索空间。
- 约束簇分析:识别具有相似特征的约束,并将它们分组,以便更有效地求解。
源码
#ifndef OR_TOOLS_SAT_CP_MODEL_PRESOLVE_H_
#define OR_TOOLS_SAT_CP_MODEL_PRESOLVE_H_
#include <array>
#include <cstdint>
#include <utility>
#include <vector>
#include "absl/base/attributes.h"
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "ortools/sat/cp_model.pb.h"
#include "ortools/sat/cp_model_utils.h"
#include "ortools/sat/presolve_context.h"
#include "ortools/sat/presolve_util.h"
#include "ortools/sat/sat_parameters.pb.h"
#include "ortools/util/affine_relation.h"
#include "ortools/util/bitset.h"
#include "ortools/util/logging.h"
#include "ortools/util/sorted_interval_list.h"
#include "ortools/util/time_limit.h"
namespace operations_research {
namespace sat {
// 替换所有变量i(以及引用它的文字)为mapping[i]。变量i的定义也被移动到其新索引。
// 负数映射值的变量将被忽略,如果任何地方引用了此类变量,则出错(这是CHECKed)。
//
// 映射的图像应在[0,new_num_variables)中密集,这也是CHECKed。
void ApplyVariableMapping(const std::vector<int>& mapping,
const PresolveContext& context);
// 对presolved_model进行预处理。
//
// 这还创建了一个模型映射,用于编码两个问题之间的对应关系。具体工作如下:
// - mapping_model的第一个变量与初始模型的变量一一对应。
// - presolved_model的变量与映射模型中postsolve_mapping指定索引处的变量一一对应。
// - 通过固定其中一个变量并求解模型,将另一个变量集分配给另一个问题的可行解。此外,这些解的目标值将相同。
// 注意,在实践中,解决这类问题所需的时间很短,因为传播基本上会完成所有工作。
//
// 注意(用户):如果优化模型可以转换为决策问题,例如目标被固定或与问题的其余部分无关,则可以将其转换为决策问题。
//
// TODO(用户):识别断开的组件并返回预处理模型的向量?如果我们采用这种方式,将索引存储在模型内部可能更好。
// 可以向模型添加IntegerVariableProto :: initial_index;
class CpModelPresolver {
public:
CpModelPresolver(PresolveContext* context,
std::vector<int>* postsolve_mapping);
// 返回预处理后的问题状态:
// - 如果一切正常,则返回UNKNOWN。
// - 如果在预处理期间证明了模型是不可行的,则返回INFEASIBLE。
// - 如果模型引起了某些问题,例如我们无法以足够的精度缩放浮点目标,则返回MODEL_INVALID。
CpSolverStatus Presolve();
// 对给定约束执行预处理方法。仅供测试使用。
bool PresolveOneConstraint(int c);
// 仅供测试使用。
void RemoveEmptyConstraints();
private:
// 记录已应用的规则并返回INFEASIBLE。
CpSolverStatus InfeasibleStatus();
// 运行预处理器的内部循环。
void PresolveToFixPoint();
// 运行探测过程。
void Probe();
// 预处理函数。
//
// 仅当约束<->变量图未更改时才应返回false。这只是一种优化,返回true始终是正确的。
//
// 不变式关于UNSAT:如果context_.IsUnsat()为true,则所有这些函数都应立即中止。
// 更改状态为不可满足的唯一方法是通过ABSL_MUST_USE_RESULT函数,该函数也应立即中止当前代码。
// 这样,我们不应在不一致状态上进行计算。
// TODO(用户):将这些公开并进行单元测试。
bool PresolveAllDiff(ConstraintProto* ct);
bool PresolveAutomaton(ConstraintProto* ct);
bool PresolveElement(ConstraintProto* ct);
bool PresolveIntAbs(ConstraintProto* ct);
bool PresolveIntDiv(ConstraintProto* ct);
bool PresolveIntMod(ConstraintProto* ct);
bool PresolveIntProd(ConstraintProto* ct);
bool PresolveInterval(int c, ConstraintProto* ct);
bool PresolveInverse(ConstraintProto* ct);
bool PresolveLinMax(ConstraintProto* ct);
bool PresolveTable(ConstraintProto* ct);
bool PresolveCumulative(ConstraintProto* ct);
bool PresolveNoOverlap(ConstraintProto* ct);
bool PresolveNoOverlap2D(int c, ConstraintProto* ct);
bool PresolveReservoir(ConstraintProto* ct);
bool PresolveCircuit(ConstraintProto* ct);
bool PresolveRoutes(ConstraintProto* ct);
bool PresolveAtMostOrExactlyOne(ConstraintProto* ct);
bool PresolveAtMostOne(ConstraintProto* ct);
bool PresolveExactlyOne(ConstraintProto* ct);
bool PresolveBoolAnd(ConstraintProto* ct);
bool PresolveBoolOr(ConstraintProto* ct);
bool PresolveBoolXor(ConstraintProto* ct);
bool PresolveEnforcementLiteral(ConstraintProto* ct);
// 对线性约束进行重组,并替换仿射关系。
// 如果表达式中的变量集合发生更改,则返回true。
template <typename ProtoWithVarsAndCoeffs>
bool CanonicalizeLinearExpressionInternal(const ConstraintProto& ct,
ProtoWithVarsAndCoeffs* proto,
int64_t* offset);
bool CanonicalizeLinearExpression(const ConstraintProto& ct,
LinearExpressionProto* exp);
bool CanonicalizeLinearArgument(const ConstraintProto& ct,
LinearArgumentProto* proto);
// 对于线性约束,我们有多个函数。
bool CanonicalizeLinear(ConstraintProto* ct);
bool PropagateDomainsInLinear(int ct_index, ConstraintProto* ct);
bool RemoveSingletonInLinear(ConstraintProto* ct);
bool PresolveSmallLinear(ConstraintProto* ct);
bool PresolveLinearOfSizeOne(ConstraintProto* ct);
bool PresolveLinearOfSizeTwo(ConstraintProto* ct);
bool PresolveLinearOnBooleans(ConstraintProto* ct);
bool PresolveDiophantine(ConstraintProto* ct);
bool AddVarAffineRepresentativeFromLinearEquality(int target_index,
ConstraintProto* ct);
bool PresolveLinearEqualityWithModulo(ConstraintProto* ct);
// 检测线性约束的子集是否在另一个线性约束中,并进行相关的预处理。
void DetectAndProcessAtMostOneInLinear(int ct_index, ConstraintProto* ct,
ActivityBoundHelper* helper);
// 如果约束的形式是“a * expr_X + expr_Y”,其中expr_Y只能取相对于a的较小值,
// 取决于边界,约束可以等效为仅针对expr_X的约束。
//
// 例如,“10'001 X + 9'999 Y <= 105'000,其中X,Y在[0,100]”可以重写为X + Y <= 10!
// 在将浮点约束缩放到整数系数时,这很容易发生。
void TryToReduceCoefficientsOfLinearConstraint(int c, ConstraintProto* ct);
// 检测并转换形式为:“X = sum Boolean * value”的约束,
// 其中“sum Boolean <= 1”。
//
// 请注意,这个函数不是特别快,因此不应该经常调用。
void ExtractEncodingFromLinear();
bool ProcessEncodingFromLinear(int linear_encoding_ct_index,
const ConstraintProto& at_most_or_exactly_one,
int64_t* num_unique_terms,
int64_t* num_multiple_terms);
// 移除重复约束。这还合并具有重复线性表达式的线性约束的定义域。
void DetectDuplicateConstraints();
// 检测线性约束是否“包含”在另一个约束中,并进行相关的预处理。
void DetectDominatedLinearConstraints();
// 如果变量x的域由当前约束隐含,则返回true。
//
// 警告:这可能会扫描x出现的所有约束条目。
// 工作量将增加与扫描的条目数相应地增加。
bool IsImpliedFree(const std::vector<int>& constraints, int x,
int64_t* work_done);
// 检测包含仅包含线性约束的两列的共享大量公共条目的情况。
// 然后,在有前途的候选项上调用PerformFreeColumnSubstitution(),
// 以减少非零元素的总数。
void DetectOverlappingColumns();
// 假设x是被暗示自由的,且仅出现在线性约束中,此时将x替换为x + factor * y。
// 这假定我们预先计算了其中出现x的所有约束以及x内的系数。
void PerformFreeColumnSubstitution(
const std::vector<std::pair<int, int64_t>>& constraints_with_x_coeff,
int x, int y, int64_t factor);
// SetPPC代表了集合打包、分割和覆盖约束。它们分别是sum of booleans <=、= 和 >= 1。
// 我们检测这些约束的包含关系,从而进行一些简化。
void ProcessSetPPC();
// 删除支配约束或为给定的两个相互包含的setppc约束修复一些变量。
bool ProcessSetPPCSubset(int subset_c, int superset_c,
absl::flat_hash_set<int>* tmp_set,
bool* remove_subset, bool* remove_superset,
bool* stop_processing_superset);
// 运行SAT特定的预处理代码。
void PresolvePureSatPart();
// 从线性约束中提取AtMostOne约束。
void ExtractAtMostOneFromLinear(ConstraintProto* ct);
// 如果约束发生了变化,则返回true。
bool DivideLinearByGcd(ConstraintProto* ct);
void ExtractEnforcementLiteralFromLinearConstraint(int ct_index,
ConstraintProto* ct);
void LowerThanCoeffStrengthening(bool from_lower_bound, int64_t min_magnitude,
int64_t threshold, ConstraintProto* ct);
// 从bool_and和small at_most_one约束中提取团,并将其转换为最大团。
void TransformIntoMaxCliques();
// 将bool_or和size为2的at_most_one转换为bool_and。
void ExtractBoolAnd();
void ExpandObjective();
void ProcessVariableOnlyUsedInEncoding(int var);
void TryToSimplifyDomain(int var);
void LookAtVariableWithDegreeTwo(int var);
void ProcessVariableInTwoAtMostOrExactlyOne(int var);
void MergeNoOverlapConstraints();
// 合并仅在一个文字上不同的子句的启发式方法。
// 这种思路是将一堆子句合并为一个bool_and。
// 这样做有几个目的:
// - 模型更小。
// - 更强大的对偶推理,因为锁定较少。
// - 如果布尔值的否定在at_most_one中至多出现一次,我们将获得更强的LP松弛度。
//
// 用户注意:如果合并过程成功,我们可能希望为这样的bool_and开发自定义推理器。
// 理论上,与子句的两个观察文字方案相比,它应该更有效率。调查一下!
void MergeClauses();
// 这两个函数都负责处理仿射关系。
// 第二个函数返回false表示UNSAT。
void EncodeAllAffineRelations();
bool PresolveAffineRelationIfAny(int var);
bool ExploitEquivalenceRelations(int c, ConstraintProto* ct);
ABSL_MUST_USE_RESULT bool RemoveConstraint(ConstraintProto* ct);
ABSL_MUST_USE_RESULT bool MarkConstraintAsFalse(ConstraintProto* ct);
std::vector<int>* postsolve_mapping_;
PresolveContext* context_;
SolverLogger* logger_;
// 用于CanonicalizeLinearExpressionInternal()。
std::vector<std::pair<int, int64_t>> tmp_terms_;
// 用于DetectAndProcessAtMostOneInLinear()。
std::vector<std::array<int64_t, 2>> conditional_mins_;
std::vector<std::array<int64_t, 2>> conditional_maxs_;
// 用于ProcessSetPPCSubset()和DetectAndProcessAtMostOneInLinear()以在内部传播具有at_most_one或exactly_one的线性。
absl::flat_hash_map<int, int> temp_map_;
absl::flat_hash_set<int> temp_set_;
ConstraintProto temp_ct_;
// 用于TryToReduceCoefficientsOfLinearConstraint()。
MaxBoundedSubsetSum lb_feasible_;
MaxBoundedSubsetSum lb_infeasible_;
MaxBoundedSubsetSum ub_feasible_;
MaxBoundedSubsetSum ub_infeasible_;
};
// 此辅助类执行从模型和部分赋值到另一个模型的复制和简化。
// 目的是最小化所复制模型的大小,以及减少对内存子系统的压力。
//
// 目前仅由LNS部分使用,但可以与任何生成部分赋值的其他方案一起使用。
class ModelCopy {
public:
explicit ModelCopy(PresolveContext* context);
// 将所有约束从in_model复制到上下文的工作模型。
//
// 在此过程中,它将从上下文中读取变量域,并简化约束以最小化所复制模型的大小。
// 因此,重要的是context->working_model已经复制了变量部分。
//
// 如果模型被证明不可行,则返回false。
//
// 它不清除上下文的工作模型的约束部分。
//
// 注意(用户):如果first_copy为true,我们将重新安排调度约束,以便它们仅引用先前定义的间隔。
// 这样可以在后续的预处理步骤中更高效地进行操作。
bool ImportAndSimplifyConstraints(const CpModelProto& in_model,
const std::vector<int>& ignored_constraints,
bool first_copy = false);
// 将变量从in_model复制到工作模型。
// 它从上下文中读取'ignore_names'参数,并相应地保留或删除名称。
void ImportVariablesAndMaybeIgnoreNames(const CpModelProto& in_model);
private:
// 覆盖out_model使其不可满足。返回false。
bool CreateUnsatModel();
void CopyEnforcementLiterals(const ConstraintProto& orig,
ConstraintProto* dest);
bool OneEnforcementLiteralIsFalse(const ConstraintProto& ct) const;
// 所有这些函数都返回false,如果约束被发现是不可行的。
bool CopyBoolOr(const ConstraintProto& ct);
bool CopyBoolOrWithDupSupport(const ConstraintProto& ct);
bool CopyBoolAnd(const ConstraintProto& ct);
bool CopyLinear(const ConstraintProto& ct);
bool CopyAtMostOne(const ConstraintProto& ct);
bool CopyExactlyOne(const ConstraintProto& ct);
bool CopyInterval(const ConstraintProto& ct, int c, bool ignore_names);
// 这些函数删除未执行的间隔。请注意,它们要求间隔先出现(经过验证),
// 因为它们测试未执行通过测试interval_mapping_是否为空。
void CopyAndMapNoOverlap(const ConstraintProto& ct);
void CopyAndMapNoOverlap2D(const ConstraintProto& ct);
void CopyAndMapCumulative(const ConstraintProto& ct);
PresolveContext* context_;
int64_t skipped_non_zero_ = 0;
// 临时向量。
std::vector<int> non_fixed_variables_;
std::vector<int64_t> non_fixed_coefficients_;
absl::flat_hash_map<int, int> interval_mapping_;
int starting_constraint_index_ = 0;
std::vector<int> temp_enforcement_literals_;
std::vector<int> temp_literals_;
absl::flat_hash_set<int> tmp_literals_set_;
};
// 将in_model复制到presolve上下文中的模型。
// 它在运行时进行简化,并且如果模型被证明不可行,则返回false。
// 它读取参数'ignore_names',并根据需要保留或删除变量和约束名称。
//
// 这只应在用户给定模型的第一个副本上调用。
// 注意,这会重新排列使用间隔的所有约束放在最后。我们失去了用户定义的顺序,但希望这不会太重要。
bool ImportModelWithBasicPresolveIntoContext(const CpModelProto& in_model,
PresolveContext* context);
// 复制除变量和约束部分以外的模型的所有内容。
void CopyEverythingExceptVariablesAndConstraintsFieldsIntoContext(
const CpModelProto& in_model, PresolveContext* context);
// 方便的包装器来调用完整的presolve。
CpSolverStatus PresolveCpModel(PresolveContext* context,
std::vector<int>* postsolve_mapping);
// 返回给定proto中重复约束的索引,
// 每对的第一个元素是重复约束中的第一个元素"代表"。
//
// 忽略空约束。我们还做了一些额外的工作:
// - 在比较约束时,我们忽略名称。
// - 对于线性约束,我们忽略域。这是因为如果约束相同,我们可以将它们合并。
// - 如果线性约束与目标平行且没有强制执法文字,我们返回特殊的kObjectiveConstraint (< 0)代表。
// 这样的约束的域可以与目标域合并。
//
// 如果ignore_enforcement为true,则忽略执行文字,但不进行线性域或目标特殊情况处理。
// 这允许处理一些其他情况,例如:
// - 实施的约束重复非执行的约束。
// - 两个实施的约束具有单个执行(vpphard)。
//
// 可见于此以进行测试。这意味着在规范化约束之后调用。
std::vector<std::pair<int, int>> FindDuplicateConstraints(
const CpModelProto& model_proto, bool ignore_enforcement = false);
} // namespace sat
} // namespace operations_research
#endif // OR_TOOLS_SAT_CP_MODEL_PRESOLVE_H_
cp_model_presolve .cc
#include "ortools/sat/cp_model_presolve.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <deque>
#include <iostream>
#include <limits>
#include <numeric>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "absl/base/attributes.h"
#include "absl/container/btree_map.h"
#include "absl/container/btree_set.h"
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/hash/hash.h"
#include "absl/numeric/int128.h"
#include "absl/strings/str_cat.h"
#include "absl/types/span.h"
#include "ortools/base/integral_types.h"
#include "ortools/base/logging.h"
#include "ortools/base/mathutil.h"
#include "ortools/base/stl_util.h"
#include "ortools/base/timer.h"
#include "ortools/sat/circuit.h"
#include "ortools/sat/clause.h"
#include "ortools/sat/cp_model.pb.h"
#include "ortools/sat/cp_model_checker.h"
#include "ortools/sat/cp_model_expand.h"
#include "ortools/sat/cp_model_mapping.h"
#include "ortools/sat/cp_model_symmetries.h"
#include "ortools/sat/cp_model_utils.h"
#include "ortools/sat/diffn_util.h"
#include "ortools/sat/diophantine.h"
#include "ortools/sat/inclusion.h"
#include "ortools/sat/integer.h"
#include "ortools/sat/model.h"
#include "ortools/sat/presolve_context.h"
#include "ortools/sat/presolve_util.h"
#include "ortools/sat/probing.h"
#include "ortools/sat/sat_base.h"
#include "ortools/sat/sat_parameters.pb.h"
#include "ortools/sat/sat_solver.h"
#include "ortools/sat/simplification.h"
#include "ortools/sat/util.h"
#include "ortools/sat/var_domination.h"
#include "ortools/util/affine_relation.h"
#include "ortools/util/bitset.h"
#include "ortools/util/logging.h"
#include "ortools/util/saturated_arithmetic.h"
#include "ortools/util/sorted_interval_list.h"
#include "ortools/util/time_limit.h"
namespace operations_research {
namespace sat {
bool CpModelPresolver::RemoveConstraint(ConstraintProto* ct) {
ct->Clear();
return true;
}
// 移除所有空的约束。需要注意重新映射区间引用。
//
// 现在这些约束已经完成了它们的目的,我们还会移除虚拟约束,否则这会导致问题,因为我们的模型在测试中是无效的。
void CpModelPresolver::RemoveEmptyConstraints() {
std::vector<int> interval_mapping(context_->working_model->constraints_size(),
-1);
int new_num_constraints = 0;
const int old_num_non_empty_constraints =
context_->working_model->constraints_size();
for (int c = 0; c < old_num_non_empty_constraints; ++c) {
const auto type = context_->working_model->constraints(c).constraint_case();
if (type == ConstraintProto::CONSTRAINT_NOT_SET) continue;
if (type == ConstraintProto::kDummyConstraint) continue;
if (type == ConstraintProto::kInterval) {
interval_mapping[c] = new_num_constraints;
}
context_->working_model->mutable_constraints(new_num_constraints++)
->Swap(context_->working_model->mutable_constraints(c));
}
context_->working_model->mutable_constraints()->DeleteSubrange(
new_num_constraints, old_num_non_empty_constraints - new_num_constraints);
for (ConstraintProto& ct_ref :
*context_->working_model->mutable_constraints()) {
ApplyToAllIntervalIndices(
[&interval_mapping](int* ref) {
*ref = interval_mapping[*ref];
CHECK_NE(-1, *ref);
},
&ct_ref);
}
}
bool CpModelPresolver::PresolveEnforcementLiteral(ConstraintProto* ct) {
if (context_->ModelIsUnsat()) return false;
if (!HasEnforcementLiteral(*ct)) return false;
int new_size = 0;
const int old_size = ct->enforcement_literal().size();
context_->tmp_literal_set.clear();
for (const int literal : ct->enforcement_literal()) {
if (context_->LiteralIsTrue(literal)) {
// 我们可以移除一个为真的文字。
context_->UpdateRuleStats("enforcement: true literal");
continue;
}
if (context_->LiteralIsFalse(literal)) {
context_->UpdateRuleStats("enforcement: false literal");
return RemoveConstraint(ct);
}
if (context_->VariableIsUniqueAndRemovable(literal)) {
// 我们可以将其设置为假并忽略此约束。
context_->UpdateRuleStats("enforcement: literal not used");
CHECK(context_->SetLiteralToFalse(literal));
return RemoveConstraint(ct);
}
// 如果文字仅出现在目标函数中,我们可能能够将其固定为假。如果文字始终以相同的极性出现,这种情况下需要进行推广。
if (context_->VariableWithCostIsUniqueAndRemovable(literal)) {
const int64_t obj_coeff =
context_->ObjectiveMap().at(PositiveRef(literal));
if (RefIsPositive(literal) == (obj_coeff > 0)) {
// 将其设置为假更有利!
context_->UpdateRuleStats("enforcement: literal with unique direction");
CHECK(context_->SetLiteralToFalse(literal));
return RemoveConstraint(ct);
}
}
// 处理重复的文字。
//
// 理想情况下,在第一次复制期间仅执行一次此操作,并且在以后永远不会创建这样的约束。
if (old_size > 1) {
const auto [_, inserted] = context_->tmp_literal_set.insert(literal);
if (!inserted) {
context_->UpdateRuleStats("enforcement: removed duplicate literal");
continue;
}
if (context_->tmp_literal_set.contains(NegatedRef(literal))) {
context_->UpdateRuleStats("enforcement: can never be true");
return RemoveConstraint(ct);
}
}
ct->set_enforcement_literal(new_size++, literal);
}
ct->mutable_enforcement_literal()->Truncate(new_size);
return new_size != old_size;
}
bool CpModelPresolver::PresolveBoolXor(ConstraintProto* ct) {
if (context_->ModelIsUnsat()) return false;
if (HasEnforcementLiteral(*ct)) return false;
int new_size = 0;
bool changed = false;
int num_true_literals = 0;
int true_literal = std::numeric_limits<int32_t>::min();
for (const int literal : ct->bool_xor().literals()) {
if (context_->VariableIsUniqueAndRemovable(literal)) {
context_->UpdateRuleStats("TODO bool_xor: remove constraint");
}
if (context_->LiteralIsFalse(literal)) {
context_->UpdateRuleStats("bool_xor: remove false literal");
changed = true;
continue;
} else if (context_->LiteralIsTrue(literal)) {
true_literal = literal; // 如果需要,保留下来。
num_true_literals++;
continue;
}
ct->mutable_bool_xor()->set_literals(new_size++, literal);
}
if (new_size == 0) {
if (num_true_literals % 2 == 0) {
return context_->NotifyThatModelIsUnsat("bool_xor: always false");
} else {
context_->UpdateRuleStats("bool_xor: always true");
return RemoveConstraint(ct);
}
} else if (new_size == 1) {
// 我们可以修复唯一的文字。
if (num_true_literals % 2 == 0) {
if (!context_->SetLiteralToTrue(ct->bool_xor().literals(0))) {
return context_->NotifyThatModelIsUnsat(
"bool_xor: cannot fix last literal");
}
} else {
if (!context_->SetLiteralToFalse(ct->bool_xor().literals(0))) {
return context_->NotifyThatModelIsUnsat(
"bool_xor: cannot fix last literal");
}
}
context_->UpdateRuleStats("bool_xor: one active literal");
return RemoveConstraint(ct);
} else if (new_size == 2) {
// 我们可以简化 bool_xor。
const int a