2214 lines
84 KiB
C++
2214 lines
84 KiB
C++
// Copyright 2010-2022 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.
|
|
|
|
#include "ortools/sat/cp_model_expand.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "absl/container/btree_map.h"
|
|
#include "absl/container/flat_hash_map.h"
|
|
#include "absl/container/flat_hash_set.h"
|
|
#include "absl/container/inlined_vector.h"
|
|
#include "absl/log/check.h"
|
|
#include "absl/meta/type_traits.h"
|
|
#include "absl/strings/str_cat.h"
|
|
#include "ortools/base/logging.h"
|
|
#include "ortools/base/stl_util.h"
|
|
#include "ortools/base/types.h"
|
|
#include "ortools/port/proto_utils.h"
|
|
#include "ortools/sat/cp_model.pb.h"
|
|
#include "ortools/sat/cp_model_utils.h"
|
|
#include "ortools/sat/presolve_context.h"
|
|
#include "ortools/sat/sat_parameters.pb.h"
|
|
#include "ortools/sat/util.h"
|
|
#include "ortools/util/logging.h"
|
|
#include "ortools/util/saturated_arithmetic.h"
|
|
#include "ortools/util/sorted_interval_list.h"
|
|
|
|
namespace operations_research {
|
|
namespace sat {
|
|
|
|
// TODO(user): Note that if we have duplicate variables controlling different
|
|
// time point, this might not reach the fixed point. Fix? it is not that
|
|
// important as the expansion take care of this case anyway.
|
|
void PropagateAutomaton(const AutomatonConstraintProto& proto,
|
|
const PresolveContext& context,
|
|
std::vector<absl::flat_hash_set<int64_t>>* states,
|
|
std::vector<absl::flat_hash_set<int64_t>>* labels) {
|
|
const int n = proto.vars_size();
|
|
const absl::flat_hash_set<int64_t> final_states(
|
|
{proto.final_states().begin(), proto.final_states().end()});
|
|
|
|
labels->clear();
|
|
labels->resize(n);
|
|
states->clear();
|
|
states->resize(n + 1);
|
|
(*states)[0].insert(proto.starting_state());
|
|
|
|
// Forward pass.
|
|
for (int time = 0; time < n; ++time) {
|
|
for (int t = 0; t < proto.transition_tail_size(); ++t) {
|
|
const int64_t tail = proto.transition_tail(t);
|
|
const int64_t label = proto.transition_label(t);
|
|
const int64_t head = proto.transition_head(t);
|
|
if (!(*states)[time].contains(tail)) continue;
|
|
if (!context.DomainContains(proto.vars(time), label)) continue;
|
|
if (time == n - 1 && !final_states.contains(head)) continue;
|
|
(*labels)[time].insert(label);
|
|
(*states)[time + 1].insert(head);
|
|
}
|
|
}
|
|
|
|
// Backward pass.
|
|
for (int time = n - 1; time >= 0; --time) {
|
|
absl::flat_hash_set<int64_t> new_states;
|
|
absl::flat_hash_set<int64_t> new_labels;
|
|
for (int t = 0; t < proto.transition_tail_size(); ++t) {
|
|
const int64_t tail = proto.transition_tail(t);
|
|
const int64_t label = proto.transition_label(t);
|
|
const int64_t head = proto.transition_head(t);
|
|
|
|
if (!(*states)[time].contains(tail)) continue;
|
|
if (!(*labels)[time].contains(label)) continue;
|
|
if (!(*states)[time + 1].contains(head)) continue;
|
|
new_labels.insert(label);
|
|
new_states.insert(tail);
|
|
}
|
|
(*labels)[time].swap(new_labels);
|
|
(*states)[time].swap(new_states);
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
|
|
void ExpandReservoir(ConstraintProto* ct, PresolveContext* context) {
|
|
if (ct->reservoir().min_level() > ct->reservoir().max_level()) {
|
|
VLOG(1) << "Empty level domain in reservoir constraint.";
|
|
return (void)context->NotifyThatModelIsUnsat();
|
|
}
|
|
|
|
const ReservoirConstraintProto& reservoir = ct->reservoir();
|
|
const int num_events = reservoir.time_exprs_size();
|
|
const int true_literal = context->GetTrueLiteral();
|
|
const auto is_active_literal = [&reservoir, true_literal](int index) {
|
|
if (reservoir.active_literals_size() == 0) return true_literal;
|
|
return reservoir.active_literals(index);
|
|
};
|
|
|
|
int num_positives = 0;
|
|
int num_negatives = 0;
|
|
for (const LinearExpressionProto& demand_expr : reservoir.level_changes()) {
|
|
const int64_t demand = context->FixedValue(demand_expr);
|
|
if (demand > 0) {
|
|
num_positives++;
|
|
} else if (demand < 0) {
|
|
num_negatives++;
|
|
}
|
|
}
|
|
|
|
absl::flat_hash_map<std::pair<int, int>, int> precedence_cache;
|
|
|
|
if (num_positives > 0 && num_negatives > 0) {
|
|
// Creates Boolean variables equivalent to (start[i] <= start[j]) i != j
|
|
for (int i = 0; i < num_events - 1; ++i) {
|
|
const int active_i = is_active_literal(i);
|
|
if (context->LiteralIsFalse(active_i)) continue;
|
|
const LinearExpressionProto& time_i = reservoir.time_exprs(i);
|
|
|
|
for (int j = i + 1; j < num_events; ++j) {
|
|
const int active_j = is_active_literal(j);
|
|
if (context->LiteralIsFalse(active_j)) continue;
|
|
const LinearExpressionProto& time_j = reservoir.time_exprs(j);
|
|
|
|
const int i_lesseq_j = context->GetOrCreateReifiedPrecedenceLiteral(
|
|
time_i, time_j, active_i, active_j);
|
|
context->working_model->mutable_variables(i_lesseq_j)
|
|
->set_name(absl::StrCat(i, " before ", j));
|
|
precedence_cache[{i, j}] = i_lesseq_j;
|
|
const int j_lesseq_i = context->GetOrCreateReifiedPrecedenceLiteral(
|
|
time_j, time_i, active_j, active_i);
|
|
context->working_model->mutable_variables(j_lesseq_i)
|
|
->set_name(absl::StrCat(j, " before ", i));
|
|
precedence_cache[{j, i}] = j_lesseq_i;
|
|
}
|
|
}
|
|
|
|
// Constrains the running level to be consistent at all time_exprs.
|
|
// For this we only add a constraint at the time a given demand
|
|
// take place. We also have a constraint for time zero if needed
|
|
// (added below).
|
|
for (int i = 0; i < num_events; ++i) {
|
|
const int active_i = is_active_literal(i);
|
|
if (context->LiteralIsFalse(active_i)) continue;
|
|
|
|
// Accumulates level_changes of all predecessors.
|
|
ConstraintProto* const level = context->working_model->add_constraints();
|
|
level->add_enforcement_literal(active_i);
|
|
|
|
// Add contributions from previous events.
|
|
int64_t offset = 0;
|
|
for (int j = 0; j < num_events; ++j) {
|
|
if (i == j) continue;
|
|
const int active_j = is_active_literal(j);
|
|
if (context->LiteralIsFalse(active_j)) continue;
|
|
|
|
const auto prec_it = precedence_cache.find({j, i});
|
|
CHECK(prec_it != precedence_cache.end());
|
|
const int prec_lit = prec_it->second;
|
|
const int64_t demand = context->FixedValue(reservoir.level_changes(j));
|
|
if (RefIsPositive(prec_lit)) {
|
|
level->mutable_linear()->add_vars(prec_lit);
|
|
level->mutable_linear()->add_coeffs(demand);
|
|
} else {
|
|
level->mutable_linear()->add_vars(prec_lit);
|
|
level->mutable_linear()->add_coeffs(-demand);
|
|
offset -= demand;
|
|
}
|
|
}
|
|
|
|
// Accounts for own demand in the domain of the sum.
|
|
const int64_t demand_i = context->FixedValue(reservoir.level_changes(i));
|
|
level->mutable_linear()->add_domain(
|
|
CapAdd(CapSub(reservoir.min_level(), demand_i), offset));
|
|
level->mutable_linear()->add_domain(
|
|
CapAdd(CapSub(reservoir.max_level(), demand_i), offset));
|
|
}
|
|
} else {
|
|
// If all level_changes have the same sign, we do not care about the order,
|
|
// just the sum.
|
|
auto* const sum =
|
|
context->working_model->add_constraints()->mutable_linear();
|
|
for (int i = 0; i < num_events; ++i) {
|
|
sum->add_vars(is_active_literal(i));
|
|
sum->add_coeffs(context->FixedValue(reservoir.level_changes(i)));
|
|
}
|
|
sum->add_domain(reservoir.min_level());
|
|
sum->add_domain(reservoir.max_level());
|
|
}
|
|
|
|
ct->Clear();
|
|
context->UpdateRuleStats("reservoir: expanded");
|
|
}
|
|
|
|
void ExpandIntMod(ConstraintProto* ct, PresolveContext* context) {
|
|
const LinearArgumentProto& int_mod = ct->int_mod();
|
|
const LinearExpressionProto& mod_expr = int_mod.exprs(1);
|
|
if (context->IsFixed(mod_expr)) return;
|
|
|
|
const LinearExpressionProto& expr = int_mod.exprs(0);
|
|
const LinearExpressionProto& target_expr = int_mod.target();
|
|
|
|
// We reduce the domain of target_expr to avoid later overflow.
|
|
if (!context->IntersectDomainWith(
|
|
target_expr, context->DomainSuperSetOf(expr).PositiveModuloBySuperset(
|
|
context->DomainSuperSetOf(mod_expr)))) {
|
|
return;
|
|
}
|
|
|
|
// Create a new constraint with the same enforcement as ct.
|
|
auto new_enforced_constraint = [&]() {
|
|
ConstraintProto* new_ct = context->working_model->add_constraints();
|
|
*new_ct->mutable_enforcement_literal() = ct->enforcement_literal();
|
|
return new_ct;
|
|
};
|
|
|
|
// div_expr = expr / mod_expr.
|
|
const int div_var = context->NewIntVar(
|
|
context->DomainSuperSetOf(expr).PositiveDivisionBySuperset(
|
|
context->DomainSuperSetOf(mod_expr)));
|
|
LinearExpressionProto div_expr;
|
|
div_expr.add_vars(div_var);
|
|
div_expr.add_coeffs(1);
|
|
|
|
LinearArgumentProto* const div_proto =
|
|
new_enforced_constraint()->mutable_int_div();
|
|
*div_proto->mutable_target() = div_expr;
|
|
*div_proto->add_exprs() = expr;
|
|
*div_proto->add_exprs() = mod_expr;
|
|
|
|
// Create prod_expr = div_expr * mod_expr.
|
|
const Domain prod_domain =
|
|
context->DomainOf(div_var)
|
|
.ContinuousMultiplicationBy(context->DomainSuperSetOf(mod_expr))
|
|
.IntersectionWith(context->DomainSuperSetOf(expr).AdditionWith(
|
|
context->DomainSuperSetOf(target_expr).Negation()));
|
|
const int prod_var = context->NewIntVar(prod_domain);
|
|
LinearExpressionProto prod_expr;
|
|
prod_expr.add_vars(prod_var);
|
|
prod_expr.add_coeffs(1);
|
|
|
|
LinearArgumentProto* const int_prod =
|
|
new_enforced_constraint()->mutable_int_prod();
|
|
*int_prod->mutable_target() = prod_expr;
|
|
*int_prod->add_exprs() = div_expr;
|
|
*int_prod->add_exprs() = mod_expr;
|
|
|
|
// expr - prod_expr = target_expr.
|
|
LinearConstraintProto* const lin =
|
|
new_enforced_constraint()->mutable_linear();
|
|
lin->add_domain(0);
|
|
lin->add_domain(0);
|
|
AddLinearExpressionToLinearConstraint(expr, 1, lin);
|
|
AddLinearExpressionToLinearConstraint(prod_expr, -1, lin);
|
|
AddLinearExpressionToLinearConstraint(target_expr, -1, lin);
|
|
|
|
ct->Clear();
|
|
context->UpdateRuleStats("int_mod: expanded");
|
|
}
|
|
|
|
// TODO(user): Move this into the presolve instead?
|
|
void ExpandIntProdWithBoolean(int bool_ref,
|
|
const LinearExpressionProto& int_expr,
|
|
const LinearExpressionProto& product_expr,
|
|
PresolveContext* context) {
|
|
ConstraintProto* const one = context->working_model->add_constraints();
|
|
one->add_enforcement_literal(bool_ref);
|
|
one->mutable_linear()->add_domain(0);
|
|
one->mutable_linear()->add_domain(0);
|
|
AddLinearExpressionToLinearConstraint(int_expr, 1, one->mutable_linear());
|
|
AddLinearExpressionToLinearConstraint(product_expr, -1,
|
|
one->mutable_linear());
|
|
|
|
ConstraintProto* const zero = context->working_model->add_constraints();
|
|
zero->add_enforcement_literal(NegatedRef(bool_ref));
|
|
zero->mutable_linear()->add_domain(0);
|
|
zero->mutable_linear()->add_domain(0);
|
|
AddLinearExpressionToLinearConstraint(product_expr, 1,
|
|
zero->mutable_linear());
|
|
}
|
|
|
|
void ExpandIntProd(ConstraintProto* ct, PresolveContext* context) {
|
|
const LinearArgumentProto& int_prod = ct->int_prod();
|
|
if (int_prod.exprs_size() != 2) return;
|
|
const LinearExpressionProto& a = int_prod.exprs(0);
|
|
const LinearExpressionProto& b = int_prod.exprs(1);
|
|
const LinearExpressionProto& p = int_prod.target();
|
|
int literal;
|
|
const bool a_is_literal = context->ExpressionIsALiteral(a, &literal);
|
|
const bool b_is_literal = context->ExpressionIsALiteral(b, &literal);
|
|
|
|
// We expand if exactly one of {a, b} is a literal. If both are literals, it
|
|
// will be presolved into a better version.
|
|
if (a_is_literal && !b_is_literal) {
|
|
ExpandIntProdWithBoolean(literal, b, p, context);
|
|
ct->Clear();
|
|
context->UpdateRuleStats("int_prod: expanded product with Boolean var");
|
|
} else if (b_is_literal) {
|
|
ExpandIntProdWithBoolean(literal, a, p, context);
|
|
ct->Clear();
|
|
context->UpdateRuleStats("int_prod: expanded product with Boolean var");
|
|
}
|
|
}
|
|
|
|
void ExpandInverse(ConstraintProto* ct, PresolveContext* context) {
|
|
const auto& f_direct = ct->inverse().f_direct();
|
|
const auto& f_inverse = ct->inverse().f_inverse();
|
|
const int n = f_direct.size();
|
|
CHECK_EQ(n, f_inverse.size());
|
|
|
|
// Make sure the domains are included in [0, n - 1).
|
|
// Note that if a variable and its negation appear, the domains will be set to
|
|
// zero here.
|
|
//
|
|
// TODO(user): Add support for UNSAT at expansion. This should create empty
|
|
// domain if UNSAT, so it should still work correctly.
|
|
absl::flat_hash_set<int> used_variables;
|
|
for (const int ref : f_direct) {
|
|
used_variables.insert(PositiveRef(ref));
|
|
if (!context->IntersectDomainWith(ref, Domain(0, n - 1))) {
|
|
VLOG(1) << "Empty domain for a variable in ExpandInverse()";
|
|
return;
|
|
}
|
|
}
|
|
for (const int ref : f_inverse) {
|
|
used_variables.insert(PositiveRef(ref));
|
|
if (!context->IntersectDomainWith(ref, Domain(0, n - 1))) {
|
|
VLOG(1) << "Empty domain for a variable in ExpandInverse()";
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If we have duplicate variables, we make sure the domain are reduced
|
|
// as the loop below might not detect incompatibilities.
|
|
if (used_variables.size() != 2 * n) {
|
|
for (int i = 0; i < n; ++i) {
|
|
for (int j = 0; j < n; ++j) {
|
|
// Note that if we don't have the same sign, both domain are at zero.
|
|
if (PositiveRef(f_direct[i]) != PositiveRef(f_inverse[j])) continue;
|
|
|
|
// We can't have i or j as value if i != j.
|
|
if (i == j) continue;
|
|
if (!context->IntersectDomainWith(
|
|
f_direct[i], Domain::FromValues({i, j}).Complement())) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reduce the domains of each variable by checking that the inverse value
|
|
// exists.
|
|
std::vector<int64_t> possible_values;
|
|
|
|
// Propagate from one vector to its counterpart.
|
|
const auto filter_inverse_domain =
|
|
[context, n, &possible_values](const auto& direct, const auto& inverse) {
|
|
// Propagate from the inverse vector to the direct vector.
|
|
for (int i = 0; i < n; ++i) {
|
|
possible_values.clear();
|
|
const Domain domain = context->DomainOf(direct[i]);
|
|
bool removed_value = false;
|
|
for (const int64_t j : domain.Values()) {
|
|
if (context->DomainOf(inverse[j]).Contains(i)) {
|
|
possible_values.push_back(j);
|
|
} else {
|
|
removed_value = true;
|
|
}
|
|
}
|
|
if (removed_value) {
|
|
if (!context->IntersectDomainWith(
|
|
direct[i], Domain::FromValues(possible_values))) {
|
|
VLOG(1) << "Empty domain for a variable in ExpandInverse()";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
// Note that this should reach the fixed point in one pass.
|
|
// However, if we have duplicate variable, I am not sure.
|
|
if (!filter_inverse_domain(f_direct, f_inverse)) return;
|
|
if (!filter_inverse_domain(f_inverse, f_direct)) return;
|
|
|
|
// Expand the inverse constraint by associating literal to var == value
|
|
// and sharing them between the direct and inverse variables.
|
|
//
|
|
// Note that this is only correct because the domain are tight now.
|
|
for (int i = 0; i < n; ++i) {
|
|
const int f_i = f_direct[i];
|
|
for (const int64_t j : context->DomainOf(f_i).Values()) {
|
|
// We have f[i] == j <=> r[j] == i;
|
|
const int r_j = f_inverse[j];
|
|
int r_j_i;
|
|
if (context->HasVarValueEncoding(r_j, i, &r_j_i)) {
|
|
context->InsertVarValueEncoding(r_j_i, f_i, j);
|
|
} else {
|
|
const int f_i_j = context->GetOrCreateVarValueEncoding(f_i, j);
|
|
context->InsertVarValueEncoding(f_i_j, r_j, i);
|
|
}
|
|
}
|
|
}
|
|
|
|
ct->Clear();
|
|
context->UpdateRuleStats("inverse: expanded");
|
|
}
|
|
|
|
// A[V] == V means for all i, V == i => A_i == i
|
|
void ExpandElementWithTargetEqualIndex(ConstraintProto* ct,
|
|
PresolveContext* context) {
|
|
const ElementConstraintProto& element = ct->element();
|
|
DCHECK_EQ(element.index(), element.target());
|
|
|
|
const int index_ref = element.index();
|
|
std::vector<int64_t> valid_indices;
|
|
for (const int64_t v : context->DomainOf(index_ref).Values()) {
|
|
if (!context->DomainContains(element.vars(v), v)) continue;
|
|
valid_indices.push_back(v);
|
|
}
|
|
if (valid_indices.size() < context->DomainOf(index_ref).Size()) {
|
|
if (!context->IntersectDomainWith(index_ref,
|
|
Domain::FromValues(valid_indices))) {
|
|
VLOG(1) << "No compatible variable domains in "
|
|
"ExpandElementWithTargetEqualIndex()";
|
|
return;
|
|
}
|
|
context->UpdateRuleStats("element: reduced index domain");
|
|
}
|
|
|
|
for (const int64_t v : context->DomainOf(index_ref).Values()) {
|
|
const int var = element.vars(v);
|
|
if (context->MinOf(var) == v && context->MaxOf(var) == v) continue;
|
|
context->AddImplyInDomain(
|
|
context->GetOrCreateVarValueEncoding(index_ref, v), var, Domain(v));
|
|
}
|
|
context->UpdateRuleStats(
|
|
"element: expanded with special case target = index");
|
|
ct->Clear();
|
|
}
|
|
|
|
// Special case if the array of the element is filled with constant values.
|
|
void ExpandConstantArrayElement(ConstraintProto* ct, PresolveContext* context) {
|
|
const ElementConstraintProto& element = ct->element();
|
|
const int index_ref = element.index();
|
|
const int target_ref = element.target();
|
|
|
|
// Index and target domain have been reduced before calling this function.
|
|
const Domain index_domain = context->DomainOf(index_ref);
|
|
const Domain target_domain = context->DomainOf(target_ref);
|
|
|
|
// This BoolOrs implements the deduction that if all index literals pointing
|
|
// to the same value in the constant array are false, then this value is no
|
|
// no longer valid for the target variable. They are created only for values
|
|
// that have multiples literals supporting them.
|
|
// Order is not important.
|
|
absl::flat_hash_map<int64_t, BoolArgumentProto*> supports;
|
|
{
|
|
absl::flat_hash_map<int64_t, int> constant_var_values_usage;
|
|
for (const int64_t v : index_domain.Values()) {
|
|
DCHECK(context->IsFixed(element.vars(v)));
|
|
const int64_t value = context->MinOf(element.vars(v));
|
|
if (++constant_var_values_usage[value] == 2) {
|
|
// First time we cross > 1.
|
|
BoolArgumentProto* const support =
|
|
context->working_model->add_constraints()->mutable_bool_or();
|
|
const int target_literal =
|
|
context->GetOrCreateVarValueEncoding(target_ref, value);
|
|
support->add_literals(NegatedRef(target_literal));
|
|
supports[value] = support;
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
// While this is not stricly needed since all value in the index will be
|
|
// covered, it allows to easily detect this fact in the presolve.
|
|
auto* exactly_one =
|
|
context->working_model->add_constraints()->mutable_exactly_one();
|
|
for (const int64_t v : index_domain.Values()) {
|
|
const int index_literal =
|
|
context->GetOrCreateVarValueEncoding(index_ref, v);
|
|
exactly_one->add_literals(index_literal);
|
|
|
|
const int64_t value = context->MinOf(element.vars(v));
|
|
const auto& it = supports.find(value);
|
|
if (it != supports.end()) {
|
|
// The encoding literal for 'value' of the target_ref has been
|
|
// created before.
|
|
const int target_literal =
|
|
context->GetOrCreateVarValueEncoding(target_ref, value);
|
|
context->AddImplication(index_literal, target_literal);
|
|
it->second->add_literals(index_literal);
|
|
} else {
|
|
// Try to reuse the literal of the index.
|
|
context->InsertVarValueEncoding(index_literal, target_ref, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
context->UpdateRuleStats("element: expanded value element");
|
|
ct->Clear();
|
|
}
|
|
|
|
// General element when the array contains non fixed variables.
|
|
void ExpandVariableElement(ConstraintProto* ct, PresolveContext* context) {
|
|
const ElementConstraintProto& element = ct->element();
|
|
const int index_ref = element.index();
|
|
const int target_ref = element.target();
|
|
const Domain index_domain = context->DomainOf(index_ref);
|
|
|
|
BoolArgumentProto* exactly_one =
|
|
context->working_model->add_constraints()->mutable_exactly_one();
|
|
|
|
for (const int64_t v : index_domain.Values()) {
|
|
const int var = element.vars(v);
|
|
const Domain var_domain = context->DomainOf(var);
|
|
const int index_lit = context->GetOrCreateVarValueEncoding(index_ref, v);
|
|
exactly_one->add_literals(index_lit);
|
|
|
|
if (var_domain.IsFixed()) {
|
|
context->AddImplyInDomain(index_lit, target_ref, var_domain);
|
|
} else {
|
|
ConstraintProto* const ct = context->working_model->add_constraints();
|
|
ct->add_enforcement_literal(index_lit);
|
|
ct->mutable_linear()->add_vars(var);
|
|
ct->mutable_linear()->add_coeffs(1);
|
|
ct->mutable_linear()->add_vars(target_ref);
|
|
ct->mutable_linear()->add_coeffs(-1);
|
|
ct->mutable_linear()->add_domain(0);
|
|
ct->mutable_linear()->add_domain(0);
|
|
}
|
|
}
|
|
|
|
context->UpdateRuleStats("element: expanded");
|
|
ct->Clear();
|
|
}
|
|
|
|
void ExpandElement(ConstraintProto* ct, PresolveContext* context) {
|
|
const ElementConstraintProto& element = ct->element();
|
|
|
|
const int index_ref = element.index();
|
|
const int target_ref = element.target();
|
|
const int size = element.vars_size();
|
|
|
|
// Reduce the domain of the index to be compatible with the array of
|
|
// variables. Note that the element constraint is 0 based.
|
|
if (!context->IntersectDomainWith(index_ref, Domain(0, size - 1))) {
|
|
VLOG(1) << "Empty domain for the index variable in ExpandElement()";
|
|
return;
|
|
}
|
|
|
|
// Special case when index = target.
|
|
if (index_ref == target_ref) {
|
|
ExpandElementWithTargetEqualIndex(ct, context);
|
|
return;
|
|
}
|
|
|
|
// Reduces the domain of the index and the target.
|
|
bool all_constants = true;
|
|
std::vector<int64_t> valid_indices;
|
|
const Domain index_domain = context->DomainOf(index_ref);
|
|
const Domain target_domain = context->DomainOf(target_ref);
|
|
Domain reached_domain;
|
|
for (const int64_t v : index_domain.Values()) {
|
|
const Domain var_domain = context->DomainOf(element.vars(v));
|
|
if (var_domain.IntersectionWith(target_domain).IsEmpty()) continue;
|
|
|
|
valid_indices.push_back(v);
|
|
reached_domain = reached_domain.UnionWith(var_domain);
|
|
if (var_domain.Min() != var_domain.Max()) {
|
|
all_constants = false;
|
|
}
|
|
}
|
|
|
|
if (valid_indices.size() < index_domain.Size()) {
|
|
if (!context->IntersectDomainWith(index_ref,
|
|
Domain::FromValues(valid_indices))) {
|
|
VLOG(1) << "No compatible variable domains in ExpandElement()";
|
|
return;
|
|
}
|
|
|
|
context->UpdateRuleStats("element: reduced index domain");
|
|
}
|
|
|
|
// We know the target_domain is not empty as this would have triggered the
|
|
// above check.
|
|
bool target_domain_changed = false;
|
|
if (!context->IntersectDomainWith(target_ref, reached_domain,
|
|
&target_domain_changed)) {
|
|
return;
|
|
}
|
|
|
|
if (target_domain_changed) {
|
|
context->UpdateRuleStats("element: reduced target domain");
|
|
}
|
|
|
|
if (all_constants) {
|
|
ExpandConstantArrayElement(ct, context);
|
|
return;
|
|
}
|
|
|
|
ExpandVariableElement(ct, context);
|
|
}
|
|
|
|
// Adds clauses so that literals[i] true <=> encoding[values[i]] true.
|
|
// This also implicitly use the fact that exactly one alternative is true.
|
|
void LinkLiteralsAndValues(const std::vector<int>& literals,
|
|
const std::vector<int64_t>& values,
|
|
const absl::flat_hash_map<int64_t, int>& encoding,
|
|
PresolveContext* context) {
|
|
CHECK_EQ(literals.size(), values.size());
|
|
|
|
// We use a map to make this method deterministic.
|
|
//
|
|
// TODO(user): Make sure this does not appear in the profile.
|
|
absl::btree_map<int, std::vector<int>> encoding_lit_to_support;
|
|
|
|
// If a value is false (i.e not possible), then the tuple with this
|
|
// value is false too (i.e not possible). Conversely, if the tuple is
|
|
// selected, the value must be selected.
|
|
for (int i = 0; i < values.size(); ++i) {
|
|
encoding_lit_to_support[encoding.at(values[i])].push_back(literals[i]);
|
|
}
|
|
|
|
// If all tuples supporting a value are false, then this value must be
|
|
// false.
|
|
for (const auto& [encoding_lit, support] : encoding_lit_to_support) {
|
|
CHECK(!support.empty());
|
|
if (support.size() == 1) {
|
|
context->StoreBooleanEqualityRelation(encoding_lit, support[0]);
|
|
} else {
|
|
BoolArgumentProto* bool_or =
|
|
context->working_model->add_constraints()->mutable_bool_or();
|
|
bool_or->add_literals(NegatedRef(encoding_lit));
|
|
for (const int lit : support) {
|
|
bool_or->add_literals(lit);
|
|
context->AddImplication(lit, encoding_lit);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the constraint literal => one_of(encoding[v]), for v in reachable_values.
|
|
// Note that all possible values are the ones appearing in encoding.
|
|
void AddImplyInReachableValues(int literal,
|
|
std::vector<int64_t>& reachable_values,
|
|
const absl::flat_hash_map<int64_t, int> encoding,
|
|
PresolveContext* context) {
|
|
gtl::STLSortAndRemoveDuplicates(&reachable_values);
|
|
if (reachable_values.size() == encoding.size()) return; // No constraint.
|
|
if (reachable_values.size() <= encoding.size() / 2) {
|
|
// Bool or encoding.
|
|
ConstraintProto* ct = context->working_model->add_constraints();
|
|
ct->add_enforcement_literal(literal);
|
|
BoolArgumentProto* bool_or = ct->mutable_bool_or();
|
|
for (const int64_t v : reachable_values) {
|
|
bool_or->add_literals(encoding.at(v));
|
|
}
|
|
} else {
|
|
// Bool and encoding.
|
|
absl::flat_hash_set<int64_t> set(reachable_values.begin(),
|
|
reachable_values.end());
|
|
ConstraintProto* ct = context->working_model->add_constraints();
|
|
ct->add_enforcement_literal(literal);
|
|
BoolArgumentProto* bool_and = ct->mutable_bool_and();
|
|
for (const auto [value, literal] : encoding) {
|
|
if (!set.contains(value)) {
|
|
bool_and->add_literals(NegatedRef(literal));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ExpandAutomaton(ConstraintProto* ct, PresolveContext* context) {
|
|
AutomatonConstraintProto& proto = *ct->mutable_automaton();
|
|
|
|
if (proto.vars_size() == 0) {
|
|
const int64_t initial_state = proto.starting_state();
|
|
for (const int64_t final_state : proto.final_states()) {
|
|
if (initial_state == final_state) {
|
|
context->UpdateRuleStats("automaton: empty and trivially feasible");
|
|
ct->Clear();
|
|
return;
|
|
}
|
|
}
|
|
return (void)context->NotifyThatModelIsUnsat(
|
|
"automaton: empty with an initial state not in the final states.");
|
|
} else if (proto.transition_label_size() == 0) {
|
|
return (void)context->NotifyThatModelIsUnsat(
|
|
"automaton: non-empty with no transition.");
|
|
}
|
|
|
|
std::vector<absl::flat_hash_set<int64_t>> reachable_states;
|
|
std::vector<absl::flat_hash_set<int64_t>> reachable_labels;
|
|
PropagateAutomaton(proto, *context, &reachable_states, &reachable_labels);
|
|
|
|
// We will model at each time step the current automaton state using Boolean
|
|
// variables. We will have n+1 time step. At time zero, we start in the
|
|
// initial state, and at time n we should be in one of the final states. We
|
|
// don't need to create Booleans at at time when there is just one possible
|
|
// state (like at time zero).
|
|
absl::flat_hash_map<int64_t, int> encoding;
|
|
absl::flat_hash_map<int64_t, int> in_encoding;
|
|
absl::flat_hash_map<int64_t, int> out_encoding;
|
|
bool removed_values = false;
|
|
|
|
const int n = proto.vars_size();
|
|
const std::vector<int> vars = {proto.vars().begin(), proto.vars().end()};
|
|
for (int time = 0; time < n; ++time) {
|
|
// All these vector have the same size. We will use them to enforce a
|
|
// local table constraint representing one step of the automaton at the
|
|
// given time.
|
|
std::vector<int64_t> in_states;
|
|
std::vector<int64_t> labels;
|
|
std::vector<int64_t> out_states;
|
|
for (int i = 0; i < proto.transition_label_size(); ++i) {
|
|
const int64_t tail = proto.transition_tail(i);
|
|
const int64_t label = proto.transition_label(i);
|
|
const int64_t head = proto.transition_head(i);
|
|
|
|
if (!reachable_states[time].contains(tail)) continue;
|
|
if (!reachable_states[time + 1].contains(head)) continue;
|
|
if (!context->DomainContains(vars[time], label)) continue;
|
|
|
|
// TODO(user): if this transition correspond to just one in-state or
|
|
// one-out state or one variable value, we could reuse the corresponding
|
|
// Boolean variable instead of creating a new one!
|
|
in_states.push_back(tail);
|
|
labels.push_back(label);
|
|
|
|
// On the last step we don't need to distinguish the output states, so
|
|
// we use zero.
|
|
out_states.push_back(time + 1 == n ? 0 : head);
|
|
}
|
|
|
|
// Deal with single tuple.
|
|
const int num_tuples = in_states.size();
|
|
if (num_tuples == 1) {
|
|
if (!context->IntersectDomainWith(vars[time], Domain(labels.front()))) {
|
|
VLOG(1) << "Infeasible automaton.";
|
|
return;
|
|
}
|
|
|
|
// Tricky: when the same variable is used more than once, the propagation
|
|
// above might not reach the fixed point, so we do need to fix literal
|
|
// at false.
|
|
std::vector<int> at_false;
|
|
for (const auto [value, literal] : in_encoding) {
|
|
if (value != in_states[0]) at_false.push_back(literal);
|
|
}
|
|
for (const int literal : at_false) {
|
|
if (!context->SetLiteralToFalse(literal)) return;
|
|
}
|
|
|
|
in_encoding.clear();
|
|
continue;
|
|
}
|
|
|
|
// Fully encode vars[time].
|
|
{
|
|
std::vector<int64_t> transitions = labels;
|
|
gtl::STLSortAndRemoveDuplicates(&transitions);
|
|
|
|
encoding.clear();
|
|
if (!context->IntersectDomainWith(
|
|
vars[time], Domain::FromValues(transitions), &removed_values)) {
|
|
VLOG(1) << "Infeasible automaton.";
|
|
return;
|
|
}
|
|
|
|
// Fully encode the variable.
|
|
// We can leave the encoding empty for fixed vars.
|
|
if (!context->IsFixed(vars[time])) {
|
|
for (const int64_t v : context->DomainOf(vars[time]).Values()) {
|
|
encoding[v] = context->GetOrCreateVarValueEncoding(vars[time], v);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Count how many time each value appear.
|
|
// We use this to reuse literals if possible.
|
|
absl::flat_hash_map<int64_t, int> in_count;
|
|
absl::flat_hash_map<int64_t, int> transition_count;
|
|
absl::flat_hash_map<int64_t, int> out_count;
|
|
for (int i = 0; i < num_tuples; ++i) {
|
|
in_count[in_states[i]]++;
|
|
transition_count[labels[i]]++;
|
|
out_count[out_states[i]]++;
|
|
}
|
|
|
|
// For each possible out states, create one Boolean variable.
|
|
//
|
|
// TODO(user): Add exactly one?
|
|
{
|
|
std::vector<int64_t> states = out_states;
|
|
gtl::STLSortAndRemoveDuplicates(&states);
|
|
|
|
out_encoding.clear();
|
|
if (states.size() == 2) {
|
|
const int var = context->NewBoolVar();
|
|
out_encoding[states[0]] = var;
|
|
out_encoding[states[1]] = NegatedRef(var);
|
|
} else if (states.size() > 2) {
|
|
struct UniqueDetector {
|
|
void Set(int64_t v) {
|
|
if (!is_unique) return;
|
|
if (is_set) {
|
|
if (v != value) is_unique = false;
|
|
} else {
|
|
is_set = true;
|
|
value = v;
|
|
}
|
|
}
|
|
bool is_set = false;
|
|
bool is_unique = true;
|
|
int64_t value = 0;
|
|
};
|
|
|
|
// Optimization to detect if we have an in state that is only matched to
|
|
// a single out state. Same with transition.
|
|
absl::flat_hash_map<int64_t, UniqueDetector> out_to_in;
|
|
absl::flat_hash_map<int64_t, UniqueDetector> out_to_transition;
|
|
for (int i = 0; i < num_tuples; ++i) {
|
|
out_to_in[out_states[i]].Set(in_states[i]);
|
|
out_to_transition[out_states[i]].Set(labels[i]);
|
|
}
|
|
|
|
for (const int64_t state : states) {
|
|
// If we have a relation in_state <=> out_state, then we can reuse
|
|
// the in Boolean and do not need to create a new one.
|
|
if (!in_encoding.empty() && out_to_in[state].is_unique) {
|
|
const int64_t unique_in = out_to_in[state].value;
|
|
if (in_count[unique_in] == out_count[state]) {
|
|
out_encoding[state] = in_encoding[unique_in];
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Same if we have an unique transition value that correspond only to
|
|
// this state.
|
|
if (!encoding.empty() && out_to_transition[state].is_unique) {
|
|
const int64_t unique_transition = out_to_transition[state].value;
|
|
if (transition_count[unique_transition] == out_count[state]) {
|
|
out_encoding[state] = encoding[unique_transition];
|
|
continue;
|
|
}
|
|
}
|
|
|
|
out_encoding[state] = context->NewBoolVar();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Simple encoding. This is enough to properly enforce the constraint, but
|
|
// it propagate less. It creates a lot less Booleans though. Note that we
|
|
// use implicit "exactly one" on the encoding and do not add any extra
|
|
// exactly one if the simple encoding is used.
|
|
//
|
|
// We currently decide which encoding to use depending on the number of new
|
|
// literals needed by the "heavy" encoding compared to the number of states
|
|
// and labels. When the automaton is small, using the full encoding is
|
|
// better, see for instance on rotating-workforce_Example789 were the simple
|
|
// encoding make the problem hard to solve but the full encoding allow the
|
|
// solver to solve it in a couple of seconds!
|
|
//
|
|
// Note that both encoding create about the same number of constraints.
|
|
const int num_involved_variables =
|
|
in_encoding.size() + encoding.size() + out_encoding.size();
|
|
const bool use_light_encoding = (num_tuples > num_involved_variables);
|
|
if (use_light_encoding && !in_encoding.empty() && !encoding.empty() &&
|
|
!out_encoding.empty()) {
|
|
// Part 1: If a in_state is selected, restrict the set of possible labels.
|
|
// We also restrict the set of possible out states, but this is not needed
|
|
// for correctness.
|
|
absl::flat_hash_map<int64_t, std::vector<int64_t>> in_to_label;
|
|
absl::flat_hash_map<int64_t, std::vector<int64_t>> in_to_out;
|
|
for (int i = 0; i < num_tuples; ++i) {
|
|
in_to_label[in_states[i]].push_back(labels[i]);
|
|
in_to_out[in_states[i]].push_back(out_states[i]);
|
|
}
|
|
for (const auto [in_value, in_literal] : in_encoding) {
|
|
AddImplyInReachableValues(in_literal, in_to_label[in_value], encoding,
|
|
context);
|
|
AddImplyInReachableValues(in_literal, in_to_out[in_value], out_encoding,
|
|
context);
|
|
}
|
|
|
|
// Part2, add all 3-clauses: (in_state, label) => out_state.
|
|
for (int i = 0; i < num_tuples; ++i) {
|
|
auto* bool_or =
|
|
context->working_model->add_constraints()->mutable_bool_or();
|
|
bool_or->add_literals(NegatedRef(in_encoding.at(in_states[i])));
|
|
bool_or->add_literals(NegatedRef(encoding.at(labels[i])));
|
|
bool_or->add_literals(out_encoding.at(out_states[i]));
|
|
}
|
|
|
|
in_encoding.swap(out_encoding);
|
|
out_encoding.clear();
|
|
continue;
|
|
}
|
|
|
|
// Create the tuple literals.
|
|
//
|
|
// TODO(user): Call and use the same heuristics as the table constraint to
|
|
// expand this small table with 3 columns (i.e. compress, negate, etc...).
|
|
std::vector<int> tuple_literals;
|
|
if (num_tuples == 2) {
|
|
const int bool_var = context->NewBoolVar();
|
|
tuple_literals.push_back(bool_var);
|
|
tuple_literals.push_back(NegatedRef(bool_var));
|
|
} else {
|
|
// Note that we do not need the ExactlyOneConstraint(tuple_literals)
|
|
// because it is already implicitly encoded since we have exactly one
|
|
// transition value. But adding one seems to help.
|
|
BoolArgumentProto* exactly_one =
|
|
context->working_model->add_constraints()->mutable_exactly_one();
|
|
for (int i = 0; i < num_tuples; ++i) {
|
|
int tuple_literal;
|
|
if (in_count[in_states[i]] == 1 && !in_encoding.empty()) {
|
|
tuple_literal = in_encoding[in_states[i]];
|
|
} else if (transition_count[labels[i]] == 1 && !encoding.empty()) {
|
|
tuple_literal = encoding[labels[i]];
|
|
} else if (out_count[out_states[i]] == 1 && !out_encoding.empty()) {
|
|
tuple_literal = out_encoding[out_states[i]];
|
|
} else {
|
|
tuple_literal = context->NewBoolVar();
|
|
}
|
|
|
|
tuple_literals.push_back(tuple_literal);
|
|
exactly_one->add_literals(tuple_literal);
|
|
}
|
|
}
|
|
|
|
if (!in_encoding.empty()) {
|
|
LinkLiteralsAndValues(tuple_literals, in_states, in_encoding, context);
|
|
}
|
|
if (!encoding.empty()) {
|
|
LinkLiteralsAndValues(tuple_literals, labels, encoding, context);
|
|
}
|
|
if (!out_encoding.empty()) {
|
|
LinkLiteralsAndValues(tuple_literals, out_states, out_encoding, context);
|
|
}
|
|
|
|
in_encoding.swap(out_encoding);
|
|
out_encoding.clear();
|
|
}
|
|
|
|
if (removed_values) {
|
|
context->UpdateRuleStats("automaton: reduced variable domains");
|
|
}
|
|
context->UpdateRuleStats("automaton: expanded");
|
|
ct->Clear();
|
|
}
|
|
|
|
void ExpandNegativeTable(ConstraintProto* ct, PresolveContext* context) {
|
|
TableConstraintProto& table = *ct->mutable_table();
|
|
const int num_vars = table.vars_size();
|
|
const int num_original_tuples = table.values_size() / num_vars;
|
|
std::vector<std::vector<int64_t>> tuples(num_original_tuples);
|
|
int count = 0;
|
|
for (int i = 0; i < num_original_tuples; ++i) {
|
|
for (int j = 0; j < num_vars; ++j) {
|
|
tuples[i].push_back(table.values(count++));
|
|
}
|
|
}
|
|
|
|
if (tuples.empty()) { // Early exit.
|
|
context->UpdateRuleStats("table: empty negated constraint");
|
|
ct->Clear();
|
|
return;
|
|
}
|
|
|
|
// Compress tuples.
|
|
std::vector<int64_t> domain_sizes;
|
|
for (int i = 0; i < num_vars; ++i) {
|
|
domain_sizes.push_back(context->DomainOf(table.vars(i)).Size());
|
|
}
|
|
CompressTuples(domain_sizes, &tuples);
|
|
|
|
// For each tuple, forbid the variables values to be this tuple.
|
|
std::vector<int> clause;
|
|
for (const std::vector<int64_t>& tuple : tuples) {
|
|
clause.clear();
|
|
for (int i = 0; i < num_vars; ++i) {
|
|
const int64_t value = tuple[i];
|
|
if (value == kTableAnyValue) continue;
|
|
|
|
const int literal =
|
|
context->GetOrCreateVarValueEncoding(table.vars(i), value);
|
|
clause.push_back(NegatedRef(literal));
|
|
}
|
|
|
|
// Note: if the clause is empty, then the model is infeasible.
|
|
BoolArgumentProto* bool_or =
|
|
context->working_model->add_constraints()->mutable_bool_or();
|
|
for (const int lit : clause) {
|
|
bool_or->add_literals(lit);
|
|
}
|
|
}
|
|
context->UpdateRuleStats("table: expanded negated constraint");
|
|
ct->Clear();
|
|
}
|
|
|
|
// Add the implications and clauses to link one variable (i.e. column) of a
|
|
// table to the literals controlling if the tuples are possible or not.
|
|
//
|
|
// We list of each tuple the possible values the variable can take.
|
|
// If the list is empty, then this encode "any value".
|
|
void ProcessOneCompressedColumn(
|
|
int variable, const std::vector<int>& tuple_literals,
|
|
const std::vector<absl::InlinedVector<int64_t, 2>>& values,
|
|
PresolveContext* context) {
|
|
DCHECK_EQ(tuple_literals.size(), values.size());
|
|
|
|
// Collect pairs of value-literal.
|
|
// Add the constraint literal => one of values.
|
|
//
|
|
// TODO(user): If we have n - 1 values, we could add the constraint that
|
|
// tuple literal => not(last_value) instead?
|
|
std::vector<std::pair<int64_t, int>> pairs;
|
|
std::vector<int> any_values_literals;
|
|
for (int i = 0; i < values.size(); ++i) {
|
|
if (values[i].empty()) {
|
|
any_values_literals.push_back(tuple_literals[i]);
|
|
continue;
|
|
}
|
|
|
|
ConstraintProto* ct = context->working_model->add_constraints();
|
|
ct->add_enforcement_literal(tuple_literals[i]);
|
|
|
|
// It is slightly better to use a bool_and if size is 1 instead of
|
|
// reconverting it at a later stage.
|
|
auto* literals =
|
|
values[i].size() == 1 ? ct->mutable_bool_and() : ct->mutable_bool_or();
|
|
for (const int64_t v : values[i]) {
|
|
DCHECK(context->DomainContains(variable, v));
|
|
literals->add_literals(context->GetOrCreateVarValueEncoding(variable, v));
|
|
pairs.emplace_back(v, tuple_literals[i]);
|
|
}
|
|
}
|
|
|
|
// Regroup literal with the same value and add for each the clause: If all the
|
|
// tuples containing a value are false, then this value must be false too.
|
|
std::vector<int> selected;
|
|
std::sort(pairs.begin(), pairs.end());
|
|
for (int i = 0; i < pairs.size();) {
|
|
selected.clear();
|
|
const int64_t value = pairs[i].first;
|
|
for (; i < pairs.size() && pairs[i].first == value; ++i) {
|
|
selected.push_back(pairs[i].second);
|
|
}
|
|
|
|
BoolArgumentProto* no_support =
|
|
context->working_model->add_constraints()->mutable_bool_or();
|
|
for (const int lit : selected) {
|
|
no_support->add_literals(lit);
|
|
}
|
|
for (const int lit : any_values_literals) {
|
|
no_support->add_literals(lit);
|
|
}
|
|
|
|
// And the "value" literal.
|
|
const int value_literal =
|
|
context->GetOrCreateVarValueEncoding(variable, value);
|
|
no_support->add_literals(NegatedRef(value_literal));
|
|
}
|
|
}
|
|
|
|
// Simpler encoding for table constraints with 2 variables.
|
|
void AddSizeTwoTable(
|
|
const std::vector<int>& vars,
|
|
const std::vector<std::vector<int64_t>>& tuples,
|
|
const std::vector<absl::flat_hash_set<int64_t>>& values_per_var,
|
|
PresolveContext* context) {
|
|
CHECK_EQ(vars.size(), 2);
|
|
const int left_var = vars[0];
|
|
const int right_var = vars[1];
|
|
if (context->DomainOf(left_var).IsFixed() ||
|
|
context->DomainOf(right_var).IsFixed()) {
|
|
// A table constraint with at most one variable not fixed is trivially
|
|
// enforced after domain reduction.
|
|
return;
|
|
}
|
|
|
|
absl::btree_map<int, std::vector<int>> left_to_right;
|
|
absl::btree_map<int, std::vector<int>> right_to_left;
|
|
|
|
for (const auto& tuple : tuples) {
|
|
const int64_t left_value(tuple[0]);
|
|
const int64_t right_value(tuple[1]);
|
|
DCHECK(context->DomainContains(left_var, left_value));
|
|
DCHECK(context->DomainContains(right_var, right_value));
|
|
|
|
const int left_literal =
|
|
context->GetOrCreateVarValueEncoding(left_var, left_value);
|
|
const int right_literal =
|
|
context->GetOrCreateVarValueEncoding(right_var, right_value);
|
|
left_to_right[left_literal].push_back(right_literal);
|
|
right_to_left[right_literal].push_back(left_literal);
|
|
}
|
|
|
|
int num_implications = 0;
|
|
int num_clause_added = 0;
|
|
int num_large_clause_added = 0;
|
|
auto add_support_constraint =
|
|
[context, &num_clause_added, &num_large_clause_added, &num_implications](
|
|
int lit, const std::vector<int>& support_literals,
|
|
int max_support_size) {
|
|
if (support_literals.size() == max_support_size) return;
|
|
if (support_literals.size() == 1) {
|
|
context->AddImplication(lit, support_literals.front());
|
|
num_implications++;
|
|
} else {
|
|
BoolArgumentProto* bool_or =
|
|
context->working_model->add_constraints()->mutable_bool_or();
|
|
for (const int support_literal : support_literals) {
|
|
bool_or->add_literals(support_literal);
|
|
}
|
|
bool_or->add_literals(NegatedRef(lit));
|
|
num_clause_added++;
|
|
if (support_literals.size() > max_support_size / 2) {
|
|
num_large_clause_added++;
|
|
}
|
|
}
|
|
};
|
|
|
|
for (const auto& it : left_to_right) {
|
|
add_support_constraint(it.first, it.second, values_per_var[1].size());
|
|
}
|
|
for (const auto& it : right_to_left) {
|
|
add_support_constraint(it.first, it.second, values_per_var[0].size());
|
|
}
|
|
VLOG(2) << "Table: 2 variables, " << tuples.size() << " tuples encoded using "
|
|
<< num_clause_added << " clauses, including "
|
|
<< num_large_clause_added << " large clauses, " << num_implications
|
|
<< " implications";
|
|
}
|
|
|
|
// A "WCSP" (weighted constraint programming) problem is usually encoded as
|
|
// a set of table, with one or more variable only there to carry a cost.
|
|
//
|
|
// If this is the case, we can do special presolving.
|
|
bool ReduceTableInPresenceOfUniqueVariableWithCosts(
|
|
std::vector<int>* vars, std::vector<std::vector<int64_t>>* tuples,
|
|
PresolveContext* context) {
|
|
const int num_vars = vars->size();
|
|
|
|
std::vector<bool> only_here_and_in_objective(num_vars, false);
|
|
std::vector<int64_t> objective_coeffs(num_vars, 0.0);
|
|
std::vector<int> new_vars;
|
|
std::vector<int> deleted_vars;
|
|
for (int var_index = 0; var_index < num_vars; ++var_index) {
|
|
const int var = (*vars)[var_index];
|
|
|
|
// We do not use VariableWithCostIsUniqueAndRemovable() since this one
|
|
// return false if the objective is constraining but we don't care here.
|
|
// Our transformation also do not loose solutions.
|
|
if (context->VariableWithCostIsUnique(var)) {
|
|
context->UpdateRuleStats("table: removed unused column with cost");
|
|
only_here_and_in_objective[var_index] = true;
|
|
objective_coeffs[var_index] =
|
|
RefIsPositive(var) ? context->ObjectiveMap().at(var)
|
|
: -context->ObjectiveMap().at(PositiveRef(var));
|
|
context->RemoveVariableFromObjective(var);
|
|
context->MarkVariableAsRemoved(var);
|
|
deleted_vars.push_back(var);
|
|
} else if (context->VarToConstraints(var).size() == 1) {
|
|
// If there is no cost, we can remove that variable using the same code by
|
|
// just setting the cost to zero.
|
|
context->UpdateRuleStats("table: removed unused column");
|
|
only_here_and_in_objective[var_index] = true;
|
|
objective_coeffs[var_index] = 0;
|
|
context->MarkVariableAsRemoved(var);
|
|
deleted_vars.push_back(var);
|
|
} else {
|
|
new_vars.push_back(var);
|
|
}
|
|
}
|
|
if (new_vars.size() == num_vars) return false;
|
|
|
|
// Rewrite the tuples.
|
|
// put the cost last.
|
|
int64_t min_cost = std::numeric_limits<int64_t>::max();
|
|
std::vector<int64_t> temp;
|
|
for (int i = 0; i < tuples->size(); ++i) {
|
|
int64_t cost = 0;
|
|
int new_size = 0;
|
|
temp.clear();
|
|
for (int var_index = 0; var_index < num_vars; ++var_index) {
|
|
const int64_t value = (*tuples)[i][var_index];
|
|
if (only_here_and_in_objective[var_index]) {
|
|
temp.push_back(value);
|
|
const int64_t objective_coeff = objective_coeffs[var_index];
|
|
cost += value * objective_coeff;
|
|
} else {
|
|
(*tuples)[i][new_size++] = value;
|
|
}
|
|
}
|
|
(*tuples)[i].resize(new_size);
|
|
(*tuples)[i].push_back(cost);
|
|
min_cost = std::min(min_cost, cost);
|
|
|
|
// Hack: we store the deleted value here so that we can properly encode
|
|
// the postsolve constraints below.
|
|
(*tuples)[i].insert((*tuples)[i].end(), temp.begin(), temp.end());
|
|
}
|
|
|
|
// Remove tuples that only differ by their cost.
|
|
// Make sure we will assign the proper value of the removed variable at
|
|
// postsolve.
|
|
{
|
|
int new_size = 0;
|
|
const int old_size = tuples->size();
|
|
std::sort(tuples->begin(), tuples->end());
|
|
for (int i = 0; i < tuples->size(); ++i) {
|
|
// If the prefix (up to new_vars.size()) is the same, skip this tuple.
|
|
if (new_size > 0) {
|
|
bool skip = true;
|
|
for (int var_index = 0; var_index < new_vars.size(); ++var_index) {
|
|
if ((*tuples)[i][var_index] != (*tuples)[new_size - 1][var_index]) {
|
|
skip = false;
|
|
break;
|
|
}
|
|
}
|
|
if (skip) continue;
|
|
}
|
|
|
|
// If this tuple is selected, then fix the removed variable value in the
|
|
// mapping model.
|
|
for (int j = 0; j < deleted_vars.size(); ++j) {
|
|
ConstraintProto* new_ct = context->mapping_model->add_constraints();
|
|
for (int var_index = 0; var_index < new_vars.size(); ++var_index) {
|
|
new_ct->add_enforcement_literal(context->GetOrCreateVarValueEncoding(
|
|
new_vars[var_index], (*tuples)[i][var_index]));
|
|
}
|
|
new_ct->mutable_linear()->add_vars(deleted_vars[j]);
|
|
new_ct->mutable_linear()->add_coeffs(1);
|
|
new_ct->mutable_linear()->add_domain(
|
|
(*tuples)[i][new_vars.size() + 1 + j]);
|
|
new_ct->mutable_linear()->add_domain(
|
|
(*tuples)[i][new_vars.size() + 1 + j]);
|
|
}
|
|
(*tuples)[i].resize(new_vars.size() + 1);
|
|
(*tuples)[new_size++] = (*tuples)[i];
|
|
}
|
|
tuples->resize(new_size);
|
|
if (new_size < old_size) {
|
|
context->UpdateRuleStats(
|
|
"table: removed duplicate tuples with different costs");
|
|
}
|
|
}
|
|
|
|
if (min_cost > 0) {
|
|
context->AddToObjectiveOffset(min_cost);
|
|
context->UpdateRuleStats("table: transferred min_cost to objective offset");
|
|
for (int i = 0; i < tuples->size(); ++i) {
|
|
(*tuples)[i].back() -= min_cost;
|
|
}
|
|
}
|
|
|
|
// This comes from the WCSP litterature. Basically, if by fixing a variable to
|
|
// a value, we have only tuples with a non-zero cost, we can substract the
|
|
// minimum cost of these tuples and transfer it to the variable cost.
|
|
//
|
|
// TODO(user): Doing this before table compression can prevent good
|
|
// compression. We should probably exploit this during compression to make
|
|
// sure we compress as much as possible, and once compressed, do it again. Or
|
|
// do it in a more general IP settings when one iterals implies that a set of
|
|
// literals with >0 cost are in EXO. We can transfer the min of their cost to
|
|
// that Boolean.
|
|
if (/*DISABLES CODE*/ (false)) {
|
|
for (int var_index = 0; var_index < new_vars.size(); ++var_index) {
|
|
absl::flat_hash_map<int64_t, int64_t> value_to_min_cost;
|
|
const int num_tuples = tuples->size();
|
|
for (int i = 0; i < num_tuples; ++i) {
|
|
const int64_t v = (*tuples)[i][var_index];
|
|
const int64_t cost = (*tuples)[i].back();
|
|
auto insert = value_to_min_cost.insert({v, cost});
|
|
if (!insert.second) {
|
|
insert.first->second = std::min(insert.first->second, cost);
|
|
}
|
|
}
|
|
for (int i = 0; i < num_tuples; ++i) {
|
|
const int64_t v = (*tuples)[i][var_index];
|
|
(*tuples)[i].back() -= value_to_min_cost.at(v);
|
|
}
|
|
for (const auto entry : value_to_min_cost) {
|
|
if (entry.second == 0) continue;
|
|
context->UpdateRuleStats("table: transferred cost to encoding");
|
|
const int value_literal = context->GetOrCreateVarValueEncoding(
|
|
new_vars[var_index], entry.first);
|
|
context->AddLiteralToObjective(value_literal, entry.second);
|
|
}
|
|
}
|
|
}
|
|
|
|
context->UpdateRuleStats(absl::StrCat(
|
|
"table: expansion with column(s) only in objective. Arity = ",
|
|
new_vars.size()));
|
|
|
|
*vars = new_vars;
|
|
return true;
|
|
}
|
|
|
|
// Important: the table and variable domains must be presolved before this
|
|
// is called. Some checks will fail otherwise.
|
|
void CompressAndExpandPositiveTable(bool last_column_is_cost,
|
|
const std::vector<int>& vars,
|
|
std::vector<std::vector<int64_t>>* tuples,
|
|
PresolveContext* context) {
|
|
const int num_tuples_before_compression = tuples->size();
|
|
|
|
// If the last column is actually the tuple cost, we compress the table like
|
|
// if this was a normal variable, but afterwards we treat it differently.
|
|
std::vector<int64_t> domain_sizes;
|
|
for (const int var : vars) {
|
|
domain_sizes.push_back(context->DomainOf(var).Size());
|
|
}
|
|
if (last_column_is_cost) {
|
|
domain_sizes.push_back(std::numeric_limits<int64_t>::max());
|
|
}
|
|
|
|
// We start by compressing the table with kTableAnyValue only.
|
|
const int compression_level = context->params().table_compression_level();
|
|
if (compression_level > 0) {
|
|
CompressTuples(domain_sizes, tuples);
|
|
}
|
|
const int num_tuples_after_first_compression = tuples->size();
|
|
|
|
// Tricky: If the table is big, it is better to compress it as much as
|
|
// possible to reduce the number of created booleans. Otherwise, the more
|
|
// verbose encoding can lead to better linear relaxation. Probably because the
|
|
// tuple literal can encode each variable as sum literal * value. Also because
|
|
// we have more direct implied bounds, which might lead to better cuts.
|
|
//
|
|
// For instance, on lot_sizing_cp_pigment15c.psp, compressing the table more
|
|
// is a lot worse (at least until we can produce better cut).
|
|
//
|
|
// TODO(user): Tweak the heuristic, maybe compute the reduction achieve and
|
|
// decide based on that.
|
|
std::vector<std::vector<absl::InlinedVector<int64_t, 2>>> compressed_table;
|
|
if (compression_level > 2 ||
|
|
(compression_level == 2 && num_tuples_after_first_compression > 1000)) {
|
|
compressed_table = FullyCompressTuples(domain_sizes, tuples);
|
|
if (compressed_table.size() < num_tuples_before_compression) {
|
|
context->UpdateRuleStats("table: fully compress tuples");
|
|
}
|
|
} else {
|
|
// Convert the kTableAnyValue to an empty list format.
|
|
for (int i = 0; i < tuples->size(); ++i) {
|
|
compressed_table.push_back({});
|
|
for (const int64_t v : (*tuples)[i]) {
|
|
if (v == kTableAnyValue) {
|
|
compressed_table.back().push_back({});
|
|
} else {
|
|
compressed_table.back().push_back({v});
|
|
}
|
|
}
|
|
}
|
|
if (compressed_table.size() < num_tuples_before_compression) {
|
|
context->UpdateRuleStats("table: compress tuples");
|
|
}
|
|
}
|
|
|
|
VLOG(2) << "Table compression"
|
|
<< " var=" << vars.size()
|
|
<< " cost=" << domain_sizes.size() - vars.size()
|
|
<< " tuples= " << num_tuples_before_compression << " -> "
|
|
<< num_tuples_after_first_compression << " -> "
|
|
<< compressed_table.size();
|
|
|
|
// Affect mznc2017_aes_opt_r10 instance!
|
|
std::sort(compressed_table.begin(), compressed_table.end());
|
|
|
|
const int num_vars = vars.size();
|
|
if (compressed_table.size() == 1) {
|
|
// Domains are propagated. We can remove the constraint.
|
|
context->UpdateRuleStats("table: one tuple");
|
|
if (last_column_is_cost) {
|
|
// TODO(user): Because we transfer the cost, this should always be zero,
|
|
// so not needed.
|
|
context->AddToObjectiveOffset(compressed_table[0].back()[0]);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Optimization. If a value is unique and appear alone in a cell, we can use
|
|
// the encoding literal for this line tuple literal instead of creating a new
|
|
// one.
|
|
std::vector<bool> has_any(num_vars, false);
|
|
std::vector<absl::flat_hash_map<int64_t, int>> var_index_to_value_count(
|
|
num_vars);
|
|
for (int i = 0; i < compressed_table.size(); ++i) {
|
|
for (int var_index = 0; var_index < num_vars; ++var_index) {
|
|
if (compressed_table[i][var_index].empty()) {
|
|
has_any[var_index] = true;
|
|
continue;
|
|
}
|
|
for (const int64_t v : compressed_table[i][var_index]) {
|
|
DCHECK_NE(v, kTableAnyValue);
|
|
DCHECK(context->DomainContains(vars[var_index], v));
|
|
var_index_to_value_count[var_index][v]++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create one Boolean variable per tuple to indicate if it can still be
|
|
// selected or not. Enforce an exactly one between them.
|
|
BoolArgumentProto* exactly_one =
|
|
context->working_model->add_constraints()->mutable_exactly_one();
|
|
|
|
int64_t num_reused_variables = 0;
|
|
std::vector<int> tuple_literals(compressed_table.size());
|
|
for (int i = 0; i < compressed_table.size(); ++i) {
|
|
bool create_new_var = true;
|
|
for (int var_index = 0; var_index < num_vars; ++var_index) {
|
|
if (has_any[var_index]) continue;
|
|
if (compressed_table[i][var_index].size() != 1) continue;
|
|
const int64_t v = compressed_table[i][var_index][0];
|
|
if (var_index_to_value_count[var_index][v] != 1) continue;
|
|
|
|
++num_reused_variables;
|
|
create_new_var = false;
|
|
tuple_literals[i] =
|
|
context->GetOrCreateVarValueEncoding(vars[var_index], v);
|
|
break;
|
|
}
|
|
if (create_new_var) {
|
|
tuple_literals[i] = context->NewBoolVar();
|
|
}
|
|
exactly_one->add_literals(tuple_literals[i]);
|
|
}
|
|
if (num_reused_variables > 0) {
|
|
context->UpdateRuleStats("table: reused literals");
|
|
}
|
|
|
|
// Set the cost to the corresponding tuple literal. If there is more than one
|
|
// cost, we just choose the first one which is the smallest one.
|
|
if (last_column_is_cost) {
|
|
for (int i = 0; i < tuple_literals.size(); ++i) {
|
|
context->AddLiteralToObjective(tuple_literals[i],
|
|
compressed_table[i].back()[0]);
|
|
}
|
|
}
|
|
|
|
std::vector<absl::InlinedVector<int64_t, 2>> column;
|
|
for (int var_index = 0; var_index < num_vars; ++var_index) {
|
|
if (context->IsFixed(vars[var_index])) continue;
|
|
|
|
column.clear();
|
|
for (int i = 0; i < tuple_literals.size(); ++i) {
|
|
column.push_back(compressed_table[i][var_index]);
|
|
}
|
|
ProcessOneCompressedColumn(vars[var_index], tuple_literals, column,
|
|
context);
|
|
}
|
|
|
|
context->UpdateRuleStats("table: expanded positive constraint");
|
|
}
|
|
|
|
// TODO(user): reinvestigate ExploreSubsetOfVariablesAndAddNegatedTables.
|
|
//
|
|
// TODO(user): if 2 table constraints share the same valid prefix, the
|
|
// tuple literals can be reused.
|
|
//
|
|
// TODO(user): investigate different encoding for prefix tables. Maybe
|
|
// we can remove the need to create tuple literals.
|
|
void ExpandPositiveTable(ConstraintProto* ct, PresolveContext* context) {
|
|
const TableConstraintProto& table = ct->table();
|
|
const int num_vars = table.vars_size();
|
|
const int num_original_tuples = table.values_size() / num_vars;
|
|
|
|
// Read tuples flat array and recreate the vector of tuples.
|
|
std::vector<int> vars(table.vars().begin(), table.vars().end());
|
|
std::vector<std::vector<int64_t>> tuples(num_original_tuples);
|
|
int count = 0;
|
|
for (int tuple_index = 0; tuple_index < num_original_tuples; ++tuple_index) {
|
|
for (int var_index = 0; var_index < num_vars; ++var_index) {
|
|
tuples[tuple_index].push_back(table.values(count++));
|
|
}
|
|
}
|
|
|
|
// Compute the set of possible values for each variable (from the table).
|
|
// Remove invalid tuples along the way.
|
|
std::vector<absl::flat_hash_set<int64_t>> values_per_var(num_vars);
|
|
int new_size = 0;
|
|
for (int tuple_index = 0; tuple_index < num_original_tuples; ++tuple_index) {
|
|
bool keep = true;
|
|
for (int var_index = 0; var_index < num_vars; ++var_index) {
|
|
const int64_t value = tuples[tuple_index][var_index];
|
|
if (!context->DomainContains(vars[var_index], value)) {
|
|
keep = false;
|
|
break;
|
|
}
|
|
}
|
|
if (keep) {
|
|
for (int var_index = 0; var_index < num_vars; ++var_index) {
|
|
values_per_var[var_index].insert(tuples[tuple_index][var_index]);
|
|
}
|
|
std::swap(tuples[tuple_index], tuples[new_size]);
|
|
new_size++;
|
|
}
|
|
}
|
|
tuples.resize(new_size);
|
|
|
|
if (tuples.empty()) {
|
|
context->UpdateRuleStats("table: empty");
|
|
return (void)context->NotifyThatModelIsUnsat();
|
|
}
|
|
|
|
// Update variable domains. It is redundant with presolve, but we could be
|
|
// here with presolve = false.
|
|
// Also counts the number of fixed variables.
|
|
int num_fixed_variables = 0;
|
|
for (int var_index = 0; var_index < num_vars; ++var_index) {
|
|
CHECK(context->IntersectDomainWith(
|
|
vars[var_index],
|
|
Domain::FromValues({values_per_var[var_index].begin(),
|
|
values_per_var[var_index].end()})));
|
|
if (context->DomainOf(vars[var_index]).IsFixed()) {
|
|
num_fixed_variables++;
|
|
}
|
|
}
|
|
|
|
if (num_fixed_variables == num_vars - 1) {
|
|
context->UpdateRuleStats("table: one variable not fixed");
|
|
ct->Clear();
|
|
return;
|
|
} else if (num_fixed_variables == num_vars) {
|
|
context->UpdateRuleStats("table: all variables fixed");
|
|
ct->Clear();
|
|
return;
|
|
}
|
|
|
|
// Tables with two variables do not need tuple literals.
|
|
//
|
|
// TODO(user): If there is an unique variable with cost, it is better to
|
|
// detect it. But if the detection fail, we should still call
|
|
// AddSizeTwoTable() unlike what happen here.
|
|
if (num_vars == 2 && !context->params().detect_table_with_cost()) {
|
|
AddSizeTwoTable(vars, tuples, values_per_var, context);
|
|
context->UpdateRuleStats(
|
|
"table: expanded positive constraint with two variables");
|
|
ct->Clear();
|
|
return;
|
|
}
|
|
|
|
bool last_column_is_cost = false;
|
|
if (context->params().detect_table_with_cost()) {
|
|
last_column_is_cost =
|
|
ReduceTableInPresenceOfUniqueVariableWithCosts(&vars, &tuples, context);
|
|
}
|
|
|
|
CompressAndExpandPositiveTable(last_column_is_cost, vars, &tuples, context);
|
|
ct->Clear();
|
|
}
|
|
|
|
bool AllDiffShouldBeExpanded(const Domain& union_of_domains,
|
|
ConstraintProto* ct, PresolveContext* context) {
|
|
const AllDifferentConstraintProto& proto = *ct->mutable_all_diff();
|
|
const int num_exprs = proto.exprs_size();
|
|
int num_fully_encoded = 0;
|
|
for (int i = 0; i < num_exprs; ++i) {
|
|
if (context->IsFullyEncoded(proto.exprs(i))) {
|
|
num_fully_encoded++;
|
|
}
|
|
}
|
|
|
|
if ((union_of_domains.Size() <= 2 * proto.exprs_size()) ||
|
|
(union_of_domains.Size() <= 32)) {
|
|
// Small domains.
|
|
return true;
|
|
}
|
|
|
|
if (num_fully_encoded == num_exprs && union_of_domains.Size() < 256) {
|
|
// All variables fully encoded, and domains are small enough.
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Replaces a constraint literal => ax + by != cte by a set of clauses.
|
|
// This is performed if the domains are small enough, and the variables are
|
|
// fully encoded.
|
|
//
|
|
// We do it during the expansion as we want the first pass of the presolve to be
|
|
// complete.
|
|
void ExpandSomeLinearOfSizeTwo(ConstraintProto* ct, PresolveContext* context) {
|
|
const LinearConstraintProto& arg = ct->linear();
|
|
if (arg.vars_size() != 2) return;
|
|
|
|
const int var1 = arg.vars(0);
|
|
const int var2 = arg.vars(1);
|
|
if (context->IsFixed(var1) || context->IsFixed(var2)) return;
|
|
|
|
const int64_t coeff1 = arg.coeffs(0);
|
|
const int64_t coeff2 = arg.coeffs(1);
|
|
const Domain reachable_rhs_superset =
|
|
context->DomainOf(var1)
|
|
.MultiplicationBy(coeff1)
|
|
.RelaxIfTooComplex()
|
|
.AdditionWith(context->DomainOf(var2)
|
|
.MultiplicationBy(coeff2)
|
|
.RelaxIfTooComplex());
|
|
const Domain infeasible_reachable_values =
|
|
reachable_rhs_superset.IntersectionWith(
|
|
ReadDomainFromProto(arg).Complement());
|
|
|
|
// We only deal with != cte constraints.
|
|
if (infeasible_reachable_values.Size() != 1) return;
|
|
|
|
// coeff1 * v1 + coeff2 * v2 != cte.
|
|
int64_t a = coeff1;
|
|
int64_t b = coeff2;
|
|
int64_t cte = infeasible_reachable_values.FixedValue();
|
|
int64_t x0 = 0;
|
|
int64_t y0 = 0;
|
|
if (!SolveDiophantineEquationOfSizeTwo(a, b, cte, x0, y0)) {
|
|
// no solution.
|
|
context->UpdateRuleStats("linear: expand always feasible ax + by != cte");
|
|
ct->Clear();
|
|
return;
|
|
}
|
|
const Domain reduced_domain =
|
|
context->DomainOf(var1)
|
|
.AdditionWith(Domain(-x0))
|
|
.InverseMultiplicationBy(b)
|
|
.IntersectionWith(context->DomainOf(var2)
|
|
.AdditionWith(Domain(-y0))
|
|
.InverseMultiplicationBy(-a));
|
|
|
|
if (reduced_domain.Size() > 16) return;
|
|
|
|
// Check if all the needed values are encoded.
|
|
// TODO(user): Do we force encoding for very small domains? Current
|
|
// experiments says no, but revisit later.
|
|
const int64_t size1 = context->DomainOf(var1).Size();
|
|
const int64_t size2 = context->DomainOf(var2).Size();
|
|
for (const int64_t z : reduced_domain.Values()) {
|
|
const int64_t value1 = x0 + b * z;
|
|
const int64_t value2 = y0 - a * z;
|
|
DCHECK(context->DomainContains(var1, value1)) << "value1 = " << value1;
|
|
DCHECK(context->DomainContains(var2, value2)) << "value2 = " << value2;
|
|
DCHECK_EQ(coeff1 * value1 + coeff2 * value2,
|
|
infeasible_reachable_values.FixedValue());
|
|
// TODO(user): Presolve if one or two variables are Boolean.
|
|
if (!context->HasVarValueEncoding(var1, value1, nullptr) || size1 == 2) {
|
|
return;
|
|
}
|
|
if (!context->HasVarValueEncoding(var2, value2, nullptr) || size2 == 2) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// All encoding literals already exist and the number of clauses to create
|
|
// is small enough. We can encode the constraint using just clauses.
|
|
for (const int64_t z : reduced_domain.Values()) {
|
|
const int64_t value1 = x0 + b * z;
|
|
const int64_t value2 = y0 - a * z;
|
|
// We cannot have both lit1 and lit2 true.
|
|
const int lit1 = context->GetOrCreateVarValueEncoding(var1, value1);
|
|
const int lit2 = context->GetOrCreateVarValueEncoding(var2, value2);
|
|
auto* bool_or =
|
|
context->working_model->add_constraints()->mutable_bool_or();
|
|
bool_or->add_literals(NegatedRef(lit1));
|
|
bool_or->add_literals(NegatedRef(lit2));
|
|
for (const int lit : ct->enforcement_literal()) {
|
|
bool_or->add_literals(NegatedRef(lit));
|
|
}
|
|
}
|
|
|
|
context->UpdateRuleStats("linear: expand small ax + by != cte");
|
|
ct->Clear();
|
|
}
|
|
|
|
// Note that we used to do that at loading time, but we prefer to do that as
|
|
// part of the presolve so that all variables are available for sharing between
|
|
// subworkers and also are accessible by the linear relaxation.
|
|
//
|
|
// TODO(user): Note that currently both encoding introduce extra solutions
|
|
// if the constraint has some enforcement literal(). We can either fix this by
|
|
// supporting enumeration on a subset of variable. Or add extra constraint to
|
|
// fix all new Boolean to false if the initial constraint is not enforced.
|
|
void ExpandComplexLinearConstraint(int c, ConstraintProto* ct,
|
|
PresolveContext* context) {
|
|
// TODO(user): We treat the linear of size 1 differently because we need them
|
|
// as is to recognize value encoding. Try to still creates needed Boolean now
|
|
// so that we can share more between the different workers. Or revisit how
|
|
// linear1 are propagated.
|
|
if (ct->linear().domain().size() <= 2) return;
|
|
if (ct->linear().vars().size() == 1) return;
|
|
|
|
const SatParameters& params = context->params();
|
|
if (params.encode_complex_linear_constraint_with_integer()) {
|
|
// Integer encoding.
|
|
//
|
|
// Here we add a slack with domain equal to rhs and transform
|
|
// expr \in rhs to expr - slack = 0
|
|
const Domain rhs = ReadDomainFromProto(ct->linear());
|
|
const int slack = context->NewIntVar(rhs);
|
|
ct->mutable_linear()->add_vars(slack);
|
|
ct->mutable_linear()->add_coeffs(-1);
|
|
ct->mutable_linear()->clear_domain();
|
|
ct->mutable_linear()->add_domain(0);
|
|
ct->mutable_linear()->add_domain(0);
|
|
} else {
|
|
// Boolean encoding.
|
|
int single_bool;
|
|
BoolArgumentProto* clause = nullptr;
|
|
std::vector<int> domain_literals;
|
|
if (ct->enforcement_literal().empty() && ct->linear().domain_size() == 4) {
|
|
// We cover the special case of no enforcement and two choices by creating
|
|
// a single Boolean.
|
|
single_bool = context->NewBoolVar();
|
|
} else {
|
|
clause = context->working_model->add_constraints()->mutable_bool_or();
|
|
for (const int ref : ct->enforcement_literal()) {
|
|
clause->add_literals(NegatedRef(ref));
|
|
}
|
|
}
|
|
|
|
// Save enforcement literals for the enumeration.
|
|
const std::vector<int> enforcement_literals(
|
|
ct->enforcement_literal().begin(), ct->enforcement_literal().end());
|
|
ct->mutable_enforcement_literal()->Clear();
|
|
for (int i = 0; i < ct->linear().domain_size(); i += 2) {
|
|
const int64_t lb = ct->linear().domain(i);
|
|
const int64_t ub = ct->linear().domain(i + 1);
|
|
|
|
int subdomain_literal;
|
|
if (clause != nullptr) {
|
|
subdomain_literal = context->NewBoolVar();
|
|
clause->add_literals(subdomain_literal);
|
|
domain_literals.push_back(subdomain_literal);
|
|
} else {
|
|
if (i == 0) domain_literals.push_back(single_bool);
|
|
subdomain_literal = i == 0 ? single_bool : NegatedRef(single_bool);
|
|
}
|
|
|
|
// Create a new constraint which is a copy of the original, but with a
|
|
// simple sub-domain and enforcement literal.
|
|
ConstraintProto* new_ct = context->working_model->add_constraints();
|
|
*new_ct = *ct;
|
|
new_ct->add_enforcement_literal(subdomain_literal);
|
|
FillDomainInProto(Domain(lb, ub), new_ct->mutable_linear());
|
|
}
|
|
|
|
// Make sure all booleans are tights when enumerating all solutions.
|
|
if (context->params().enumerate_all_solutions() &&
|
|
!enforcement_literals.empty()) {
|
|
int linear_is_enforced;
|
|
if (enforcement_literals.size() == 1) {
|
|
linear_is_enforced = enforcement_literals[0];
|
|
} else {
|
|
linear_is_enforced = context->NewBoolVar();
|
|
BoolArgumentProto* maintain_linear_is_enforced =
|
|
context->working_model->add_constraints()->mutable_bool_or();
|
|
for (const int e_lit : enforcement_literals) {
|
|
context->AddImplication(NegatedRef(e_lit),
|
|
NegatedRef(linear_is_enforced));
|
|
maintain_linear_is_enforced->add_literals(NegatedRef(e_lit));
|
|
}
|
|
maintain_linear_is_enforced->add_literals(linear_is_enforced);
|
|
}
|
|
|
|
for (const int lit : domain_literals) {
|
|
context->AddImplication(NegatedRef(linear_is_enforced),
|
|
NegatedRef(lit));
|
|
}
|
|
}
|
|
ct->Clear();
|
|
}
|
|
|
|
context->UpdateRuleStats("linear: expanded complex rhs");
|
|
context->InitializeNewDomains();
|
|
context->UpdateNewConstraintsVariableUsage();
|
|
context->UpdateConstraintVariableUsage(c);
|
|
}
|
|
|
|
bool IsVarEqOrNeqValue(PresolveContext* context,
|
|
const LinearConstraintProto& lin) {
|
|
if (lin.vars_size() != 1) return false;
|
|
const Domain rhs = ReadDomainFromProto(lin);
|
|
if (rhs.IsFixed()) return true;
|
|
return rhs.InverseMultiplicationBy(lin.coeffs(0))
|
|
.Complement()
|
|
.IntersectionWith(context->DomainOf(lin.vars(0)))
|
|
.IsFixed();
|
|
}
|
|
|
|
// This method will scan all constraints of all variables appearing in an
|
|
// all_diff.
|
|
// There are 3 outcomes:
|
|
// - maybe expand to Boolean variables (depending on the size)
|
|
// - keep integer all_different constraint (and cuts)
|
|
// - expand and keep
|
|
//
|
|
// Expand is selected if the variable is fully encoded, or will be when
|
|
// expanding other constraints: index of element, table, automaton.
|
|
// It will check AllDiffShouldBeExpanded() before doing the actual expansion.
|
|
// Keep is forced is the variable appears in a linear equation with at least 3
|
|
// terms, and with a tight domain ( == cst).
|
|
// TODO(user): The above rule is complex. Revisit.
|
|
void ScanModelAndDecideAllDiffExpansion(
|
|
ConstraintProto* all_diff_ct, PresolveContext* context,
|
|
absl::flat_hash_set<int>& domain_of_var_is_used,
|
|
absl::flat_hash_set<int>& bounds_of_var_are_used,
|
|
absl::flat_hash_set<int>& processed_variables, bool& expand, bool& keep) {
|
|
CHECK_EQ(all_diff_ct->constraint_case(), ConstraintProto::kAllDiff);
|
|
|
|
bool at_least_one_var_domain_is_used = false;
|
|
bool at_least_one_var_bound_is_used = false;
|
|
|
|
// Scan variables.
|
|
for (const LinearExpressionProto& expr : all_diff_ct->all_diff().exprs()) {
|
|
// Skip constant expressions.
|
|
if (expr.vars().empty()) continue;
|
|
DCHECK_EQ(1, expr.vars_size());
|
|
const int var = expr.vars(0);
|
|
DCHECK(RefIsPositive(var));
|
|
if (context->IsFixed(var)) continue;
|
|
|
|
bool at_least_one_var_domain_is_used = false;
|
|
bool at_least_one_var_bound_is_used = false;
|
|
|
|
// Check cache.
|
|
if (!processed_variables.insert(var).second) {
|
|
at_least_one_var_domain_is_used = bounds_of_var_are_used.contains(var);
|
|
at_least_one_var_bound_is_used = domain_of_var_is_used.contains(var);
|
|
} else {
|
|
bool domain_is_used = false;
|
|
bool bounds_are_used = false;
|
|
|
|
// Note: Boolean constraints are ignored.
|
|
for (const int ct_index : context->VarToConstraints(var)) {
|
|
// Skip artificial constraints.
|
|
if (ct_index < 0) continue;
|
|
|
|
const ConstraintProto& other_ct =
|
|
context->working_model->constraints(ct_index);
|
|
switch (other_ct.constraint_case()) {
|
|
case ConstraintProto::ConstraintCase::kBoolOr:
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kBoolAnd:
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kAtMostOne:
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kExactlyOne:
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kBoolXor:
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kIntDiv:
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kIntMod:
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kLinMax:
|
|
bounds_are_used = true;
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kIntProd:
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kLinear:
|
|
if (IsVarEqOrNeqValue(context, other_ct.linear()) &&
|
|
var == other_ct.linear().vars(0)) {
|
|
// Encoding literals.
|
|
domain_is_used = true;
|
|
} else if (other_ct.linear().vars_size() > 2 &&
|
|
other_ct.linear().domain_size() == 2 &&
|
|
other_ct.linear().domain(0) ==
|
|
other_ct.linear().domain(1)) {
|
|
// We assume all_diff cuts will only be useful if the linear
|
|
// constraint has a fixed domain.
|
|
bounds_are_used = true;
|
|
}
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kAllDiff:
|
|
// We ignore all_diffs as we are trying to decide their expansion
|
|
// from the rest of the model.
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kDummyConstraint:
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kElement:
|
|
// Note: elements should have been expanded.
|
|
if (other_ct.element().index() == var) {
|
|
domain_is_used = true;
|
|
}
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kCircuit:
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kRoutes:
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kInverse:
|
|
domain_is_used = true;
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kReservoir:
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kTable:
|
|
domain_is_used = true;
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kAutomaton:
|
|
domain_is_used = true;
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kInterval:
|
|
bounds_are_used = true;
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kNoOverlap:
|
|
// Will be covered by the interval case.
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kNoOverlap2D:
|
|
// Will be covered by the interval case.
|
|
break;
|
|
case ConstraintProto::ConstraintCase::kCumulative:
|
|
// Will be covered by the interval case.
|
|
break;
|
|
case ConstraintProto::ConstraintCase::CONSTRAINT_NOT_SET:
|
|
break;
|
|
}
|
|
|
|
// Exit early.
|
|
if (domain_is_used && bounds_are_used) break;
|
|
} // Loop on other_ct.
|
|
|
|
// Update cache.
|
|
if (domain_is_used) domain_of_var_is_used.insert(var);
|
|
if (bounds_are_used) bounds_of_var_are_used.insert(var);
|
|
|
|
// Update the usage of the variable.
|
|
at_least_one_var_domain_is_used |= domain_is_used;
|
|
at_least_one_var_bound_is_used |= bounds_are_used;
|
|
} // End of model scanning.
|
|
|
|
if (at_least_one_var_domain_is_used && at_least_one_var_bound_is_used) {
|
|
break; // No need to scan the rest of the all_diff.
|
|
}
|
|
} // End of var processing.
|
|
|
|
expand = at_least_one_var_domain_is_used;
|
|
keep = at_least_one_var_bound_is_used;
|
|
}
|
|
|
|
void MaybeExpandAllDiff(ConstraintProto* ct, PresolveContext* context,
|
|
absl::flat_hash_set<int>& domain_of_var_is_used,
|
|
absl::flat_hash_set<int>& bounds_of_var_are_used,
|
|
absl::flat_hash_set<int>& processed_variable) {
|
|
const bool expand_all_diff_from_parameters =
|
|
context->params().expand_alldiff_constraints();
|
|
AllDifferentConstraintProto& proto = *ct->mutable_all_diff();
|
|
if (proto.exprs_size() <= 1) return;
|
|
|
|
bool keep_after_expansion = false;
|
|
bool expand_all_diff_from_usage = false;
|
|
ScanModelAndDecideAllDiffExpansion(
|
|
ct, context, domain_of_var_is_used, bounds_of_var_are_used,
|
|
processed_variable, expand_all_diff_from_usage, keep_after_expansion);
|
|
|
|
const int num_exprs = proto.exprs_size();
|
|
Domain union_of_domains = context->DomainSuperSetOf(proto.exprs(0));
|
|
for (int i = 1; i < num_exprs; ++i) {
|
|
union_of_domains =
|
|
union_of_domains.UnionWith(context->DomainSuperSetOf(proto.exprs(i)));
|
|
}
|
|
|
|
const bool expand_all_diff_from_size =
|
|
AllDiffShouldBeExpanded(union_of_domains, ct, context);
|
|
|
|
// Decide expansion:
|
|
// - always expand if expand_all_diff_from_parameters
|
|
// - expand if size is compatible (expand_all_diff_from_size) and
|
|
// expansion is desired:
|
|
// expand_all_diff_from_usage || !keep_after_expansion
|
|
const bool should_expand =
|
|
expand_all_diff_from_parameters ||
|
|
(expand_all_diff_from_size &&
|
|
(expand_all_diff_from_usage || !keep_after_expansion));
|
|
if (!should_expand) return;
|
|
|
|
const bool is_a_permutation = num_exprs == union_of_domains.Size();
|
|
|
|
// Collect all possible variables that can take each value, and add one linear
|
|
// equation per value stating that this value can be assigned at most once, or
|
|
// exactly once in case of permutation.
|
|
for (const int64_t v : union_of_domains.Values()) {
|
|
// Collect references which domain contains v.
|
|
std::vector<LinearExpressionProto> possible_exprs;
|
|
int fixed_expression_count = 0;
|
|
for (const LinearExpressionProto& expr : proto.exprs()) {
|
|
if (!context->DomainContains(expr, v)) continue;
|
|
possible_exprs.push_back(expr);
|
|
if (context->IsFixed(expr)) {
|
|
fixed_expression_count++;
|
|
}
|
|
}
|
|
|
|
if (fixed_expression_count > 1) {
|
|
// Violates the definition of AllDifferent.
|
|
return (void)context->NotifyThatModelIsUnsat();
|
|
} else if (fixed_expression_count == 1) {
|
|
// Remove values from other domains.
|
|
for (const LinearExpressionProto& expr : possible_exprs) {
|
|
if (context->IsFixed(expr)) continue;
|
|
if (!context->IntersectDomainWith(expr, Domain(v).Complement())) {
|
|
VLOG(1) << "Empty domain for a variable in MaybeExpandAllDiff()";
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
BoolArgumentProto* at_most_or_equal_one =
|
|
is_a_permutation
|
|
? context->working_model->add_constraints()->mutable_exactly_one()
|
|
: context->working_model->add_constraints()->mutable_at_most_one();
|
|
for (const LinearExpressionProto& expr : possible_exprs) {
|
|
// The above propagation can remove a value after the expressions was
|
|
// added to possible_exprs.
|
|
if (!context->DomainContains(expr, v)) continue;
|
|
|
|
// If the expression is fixed, the created literal will be the true
|
|
// literal. We still need to fail if two expressions are fixed to the same
|
|
// value.
|
|
const int encoding = context->GetOrCreateAffineValueEncoding(expr, v);
|
|
at_most_or_equal_one->add_literals(encoding);
|
|
}
|
|
}
|
|
|
|
context->UpdateRuleStats(
|
|
absl::StrCat("all_diff:", is_a_permutation ? " permutation" : "",
|
|
" expanded", keep_after_expansion ? " and kept" : ""));
|
|
if (!keep_after_expansion) ct->Clear();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void ExpandCpModel(PresolveContext* context) {
|
|
if (context->params().disable_constraint_expansion()) return;
|
|
if (context->ModelIsUnsat()) return;
|
|
|
|
// None of the function here need to be run twice. This is because we never
|
|
// create constraint that need to be expanded during presolve.
|
|
if (context->ModelIsExpanded()) return;
|
|
|
|
// Make sure all domains are initialized.
|
|
context->InitializeNewDomains();
|
|
|
|
// Clear the precedence cache.
|
|
context->ClearPrecedenceCache();
|
|
|
|
bool has_all_diffs = false;
|
|
|
|
// First pass: we look at constraints that may fully encode variables.
|
|
for (int c = 0; c < context->working_model->constraints_size(); ++c) {
|
|
ConstraintProto* const ct = context->working_model->mutable_constraints(c);
|
|
bool skip = false;
|
|
switch (ct->constraint_case()) {
|
|
case ConstraintProto::kLinear:
|
|
// If we only do expansion, we do that as part of the main loop.
|
|
// This way we don't need to call FinalExpansionForLinearConstraint().
|
|
if (ct->linear().domain().size() > 2 &&
|
|
!context->params().cp_model_presolve()) {
|
|
ExpandComplexLinearConstraint(c, ct, context);
|
|
}
|
|
break;
|
|
case ConstraintProto::kReservoir:
|
|
if (context->params().expand_reservoir_constraints()) {
|
|
for (const LinearExpressionProto& demand_expr :
|
|
ct->reservoir().level_changes()) {
|
|
if (!context->IsFixed(demand_expr)) {
|
|
skip = true;
|
|
break;
|
|
}
|
|
}
|
|
if (skip) {
|
|
context->UpdateRuleStats(
|
|
"reservoir: expansion is not supported with variable level "
|
|
"changes");
|
|
} else {
|
|
ExpandReservoir(ct, context);
|
|
}
|
|
}
|
|
break;
|
|
case ConstraintProto::kIntMod:
|
|
ExpandIntMod(ct, context);
|
|
break;
|
|
case ConstraintProto::kIntProd:
|
|
ExpandIntProd(ct, context);
|
|
break;
|
|
case ConstraintProto::kElement:
|
|
ExpandElement(ct, context);
|
|
break;
|
|
case ConstraintProto::kInverse:
|
|
ExpandInverse(ct, context);
|
|
break;
|
|
case ConstraintProto::kAutomaton:
|
|
ExpandAutomaton(ct, context);
|
|
break;
|
|
case ConstraintProto::kTable:
|
|
if (ct->table().negated()) {
|
|
ExpandNegativeTable(ct, context);
|
|
} else {
|
|
ExpandPositiveTable(ct, context);
|
|
}
|
|
break;
|
|
case ConstraintProto::kAllDiff:
|
|
has_all_diffs = true;
|
|
skip = true;
|
|
break;
|
|
default:
|
|
skip = true;
|
|
break;
|
|
}
|
|
if (skip) continue; // Nothing was done for this constraint.
|
|
|
|
// Update variable-constraint graph.
|
|
context->UpdateNewConstraintsVariableUsage();
|
|
if (ct->constraint_case() == ConstraintProto::CONSTRAINT_NOT_SET) {
|
|
context->UpdateConstraintVariableUsage(c);
|
|
}
|
|
|
|
// Early exit if the model is unsat.
|
|
if (context->ModelIsUnsat()) {
|
|
SOLVER_LOG(context->logger(), "UNSAT after expansion of ",
|
|
ProtobufShortDebugString(*ct));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Second pass. We may decide to expand constraints if all their variables
|
|
// are fully encoded.
|
|
//
|
|
// Cache for variable scanning.
|
|
absl::flat_hash_set<int> domain_of_var_is_used;
|
|
absl::flat_hash_set<int> bounds_of_var_are_used;
|
|
absl::flat_hash_set<int> processed_variables;
|
|
for (int i = 0; i < context->working_model->constraints_size(); ++i) {
|
|
ConstraintProto* const ct = context->working_model->mutable_constraints(i);
|
|
bool skip = false;
|
|
switch (ct->constraint_case()) {
|
|
case ConstraintProto::kAllDiff:
|
|
MaybeExpandAllDiff(ct, context, domain_of_var_is_used,
|
|
bounds_of_var_are_used, processed_variables);
|
|
break;
|
|
case ConstraintProto::kLinear:
|
|
ExpandSomeLinearOfSizeTwo(ct, context);
|
|
break;
|
|
default:
|
|
skip = true;
|
|
break;
|
|
}
|
|
|
|
if (skip) continue; // Nothing was done for this constraint.
|
|
|
|
// Update variable-constraint graph.
|
|
context->UpdateNewConstraintsVariableUsage();
|
|
if (ct->constraint_case() == ConstraintProto::CONSTRAINT_NOT_SET) {
|
|
context->UpdateConstraintVariableUsage(i);
|
|
}
|
|
|
|
// Early exit if the model is unsat.
|
|
if (context->ModelIsUnsat()) {
|
|
SOLVER_LOG(context->logger(), "UNSAT after expansion of ",
|
|
ProtobufShortDebugString(*ct));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// The precedence cache can become invalid during presolve as it does not
|
|
// handle variable substitution. It is safer just to clear it at the end
|
|
// of the expansion phase.
|
|
context->ClearPrecedenceCache();
|
|
|
|
// Make sure the context is consistent.
|
|
context->InitializeNewDomains();
|
|
|
|
// Update any changed domain from the context.
|
|
for (int i = 0; i < context->working_model->variables_size(); ++i) {
|
|
FillDomainInProto(context->DomainOf(i),
|
|
context->working_model->mutable_variables(i));
|
|
}
|
|
|
|
context->NotifyThatModelIsExpanded();
|
|
}
|
|
|
|
void FinalExpansionForLinearConstraint(PresolveContext* context) {
|
|
if (context->params().disable_constraint_expansion()) return;
|
|
if (context->ModelIsUnsat()) return;
|
|
for (int c = 0; c < context->working_model->constraints_size(); ++c) {
|
|
ConstraintProto* const ct = context->working_model->mutable_constraints(c);
|
|
switch (ct->constraint_case()) {
|
|
case ConstraintProto::kLinear:
|
|
if (ct->linear().domain().size() > 2) {
|
|
ExpandComplexLinearConstraint(c, ct, context);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace sat
|
|
} // namespace operations_research
|