Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
implied_bounds.cc
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <stddef.h>
17#include <stdint.h>
18
19#include <algorithm>
20#include <array>
21#include <bitset>
22#include <string>
23#include <utility>
24#include <vector>
25
26#include "absl/container/btree_map.h"
27#include "absl/container/flat_hash_map.h"
28#include "absl/container/flat_hash_set.h"
29#include "absl/log/check.h"
30#include "absl/log/log.h"
31#include "absl/log/vlog_is_on.h"
32#include "absl/meta/type_traits.h"
33#include "absl/types/span.h"
37#include "ortools/sat/clause.h"
38#include "ortools/sat/integer.h"
41#include "ortools/sat/model.h"
46#include "ortools/util/bitset.h"
49
50namespace operations_research {
51namespace sat {
52
53// Just display some global statistics on destruction.
55 if (!VLOG_IS_ON(1)) return;
56 if (shared_stats_ == nullptr) return;
57 std::vector<std::pair<std::string, int64_t>> stats;
58 stats.push_back({"implied_bound/num_deductions", num_deductions_});
59 stats.push_back({"implied_bound/num_stored", bounds_.size()});
60 stats.push_back({"implied_bound/num_promoted_to_equivalence",
61 num_promoted_to_equivalence_});
62 stats.push_back(
63 {"implied_bound/num_stored_with_view", num_enqueued_in_var_to_bounds_});
64 stats.push_back({"implied_bound/max_changed_domain_complexity",
65 max_changed_domain_complexity_});
66 shared_stats_->AddStats(stats);
67}
68
69bool ImpliedBounds::Add(Literal literal, IntegerLiteral integer_literal) {
70 if (!parameters_.use_implied_bounds()) return true;
71 const IntegerVariable var = integer_literal.var;
72
73 // Ignore any Add() with a bound worse than the level zero one.
74 // TODO(user): Check that this never happen? it shouldn't.
75 const IntegerValue root_lb = integer_trail_->LevelZeroLowerBound(var);
76 if (integer_literal.bound <= root_lb) return true;
77
78 if (integer_literal.bound > integer_trail_->LevelZeroUpperBound(var)) {
79 // The literal being true is incompatible with the root level bounds.
80 sat_solver_->AddClauseDuringSearch({literal.Negated()});
81 return true;
82 }
83
84 // We skip any IntegerLiteral referring to a variable with only two
85 // consecutive possible values. This is because, once shifted this will
86 // already be a variable in [0, 1] so we shouldn't gain much by substituing
87 // it.
88 if (root_lb + 1 >= integer_trail_->LevelZeroUpperBound(var)) return true;
89
90 // Add or update the current bound.
91 const auto key = std::make_pair(literal.Index(), var);
92 auto insert_result = bounds_.insert({key, integer_literal.bound});
93 if (!insert_result.second) {
94 if (insert_result.first->second < integer_literal.bound) {
95 insert_result.first->second = integer_literal.bound;
96 } else {
97 // No new info.
98 return true;
99 }
100 }
101
102 // Checks if the variable is now fixed.
103 if (integer_trail_->LevelZeroUpperBound(var) == integer_literal.bound) {
104 AddLiteralImpliesVarEqValue(literal, var, integer_literal.bound);
105 } else {
106 const auto it =
107 bounds_.find(std::make_pair(literal.Index(), NegationOf(var)));
108 if (it != bounds_.end() && it->second == -integer_literal.bound) {
109 AddLiteralImpliesVarEqValue(literal, var, integer_literal.bound);
110 }
111 }
112
113 // Check if we have any deduction. Since at least one of (literal,
114 // literal.Negated()) must be true, we can take the min bound as valid at
115 // level zero.
116 const auto it = bounds_.find(std::make_pair(literal.NegatedIndex(), var));
117 if (it != bounds_.end()) {
118 if (it->second <= root_lb) {
119 // The other bounds is worse than the new level-zero bound which can
120 // happen because of lazy update, so here we just remove it.
121 bounds_.erase(it);
122 } else {
123 const IntegerValue deduction =
124 std::min(integer_literal.bound, it->second);
125 DCHECK_GT(deduction, root_lb);
126
127 ++num_deductions_;
128 if (!integer_trail_->RootLevelEnqueue(
129 IntegerLiteral::GreaterOrEqual(var, deduction))) {
130 return false;
131 }
132
133 VLOG(2) << "Deduction old: "
135 var, integer_trail_->LevelZeroLowerBound(var))
136 << " new: " << IntegerLiteral::GreaterOrEqual(var, deduction);
137
138 // The entries that are equal to the min no longer need to be stored once
139 // the level zero bound is enqueued.
140 if (it->second == deduction) {
141 bounds_.erase(it);
142 }
143 if (integer_literal.bound == deduction) {
144 bounds_.erase(std::make_pair(literal.Index(), var));
145
146 // No need to update var_to_bounds_ in this case.
147 return true;
148 }
149 // We already tested this, but enqueueing at root level can make this true
150 // again if there are holes in the domain.
151 if (integer_literal.bound <= integer_trail_->LevelZeroLowerBound(var)) {
152 return true;
153 }
154 }
155 }
156
157 const auto it2 =
158 bounds_.find(std::make_pair(literal.NegatedIndex(), NegationOf(var)));
159 // If we have "l => (x >= 9)" and "~l => (x <= 6)" we can push
160 // "l <=> (x <= 6)" to the encoded integer literals and deduce that [7, 8] is
161 // a hole in the domain. More generally, if we have:
162 //
163 // l => (x >= a)
164 // ~l => (x <= b)
165 //
166 // And if moreover b < a, we have the following truth table:
167 //
168 // l | x <= b | b < x < a | x >= a
169 // --+-----------+---------------+----------
170 // 0 | true | false | false (from "~l => (x <= b)")
171 // 1 | false | false | true (from " l => (x >= a)")
172 //
173 // So we can generalize the expressions to equivalences:
174 // l <=> (x >= a)
175 // ~l <=> (x <= b)
176 // (b < x < a) is impossible (a hole in the domain).
177 //
178 // TODO(user): understand why we need to restrict to level zero.
179 if (it2 != bounds_.end() && -it2->second < integer_literal.bound &&
180 sat_solver_->CurrentDecisionLevel() == 0) {
181 const IntegerLiteral other_integer_literal =
183 if (integer_encoder_->GetAssociatedLiteral(integer_literal) !=
184 literal.Index() ||
185 integer_encoder_->GetAssociatedLiteral(other_integer_literal) !=
186 literal.NegatedIndex()) {
187 ++num_promoted_to_equivalence_;
188 integer_encoder_->AssociateToIntegerLiteral(literal, integer_literal);
189 integer_encoder_->AssociateToIntegerLiteral(literal.Negated(),
190 other_integer_literal);
191 const IntegerValue other_bound = -it2->second;
192 if (integer_literal.bound - other_bound > 1) {
193 const Domain old_domain = integer_trail_->InitialVariableDomain(var);
194 const Domain new_domain = old_domain.IntersectionWith(
195 Domain(other_bound.value() + 1, integer_literal.bound.value() - 1)
196 .Complement());
197 max_changed_domain_complexity_ =
198 std::max(max_changed_domain_complexity_, new_domain.NumIntervals());
199 if (!integer_trail_->UpdateInitialDomain(var, new_domain)) {
200 return false;
201 }
202 }
203 }
204 }
205
206 // The information below is currently only used for cuts.
207 // So no need to store it if we aren't going to use it.
208 if (parameters_.linearization_level() == 0) return true;
209 if (parameters_.cut_level() == 0) return true;
210
211 // If we have a new implied bound and the literal has a view, add it to
212 // var_to_bounds_. Note that we might add more than one entry with the same
213 // literal_view, and we will later need to lazily clean the vector up.
214 if (integer_encoder_->GetLiteralView(literal) != kNoIntegerVariable) {
215 if (var_to_bounds_.size() <= var) {
216 var_to_bounds_.resize(var.value() + 1);
217 has_implied_bounds_.Resize(var + 1);
218 }
219 ++num_enqueued_in_var_to_bounds_;
220 has_implied_bounds_.Set(var);
221 var_to_bounds_[var].emplace_back(integer_encoder_->GetLiteralView(literal),
222 integer_literal.bound);
223 } else if (integer_encoder_->GetLiteralView(literal.Negated()) !=
225 if (var_to_bounds_.size() <= var) {
226 var_to_bounds_.resize(var.value() + 1);
227 has_implied_bounds_.Resize(var + 1);
228 }
229 ++num_enqueued_in_var_to_bounds_;
230 has_implied_bounds_.Set(var);
231 var_to_bounds_[var].emplace_back(
232 NegationOf(integer_encoder_->GetLiteralView(literal.Negated())),
233 integer_literal.bound);
234 }
235 return true;
236}
237
238const std::vector<ImpliedBoundEntry>& ImpliedBounds::GetImpliedBounds(
239 IntegerVariable var) {
240 if (var >= var_to_bounds_.size()) return empty_implied_bounds_;
241
242 // Lazily remove obsolete entries from the vector.
243 //
244 // TODO(user): Check no duplicate and remove old entry if the enforcement
245 // is tighter.
246 int new_size = 0;
247 std::vector<ImpliedBoundEntry>& ref = var_to_bounds_[var];
248 const IntegerValue root_lb = integer_trail_->LevelZeroLowerBound(var);
249 for (const ImpliedBoundEntry& entry : ref) {
250 if (entry.lower_bound <= root_lb) continue;
251 ref[new_size++] = entry;
252 }
253 ref.resize(new_size);
254
255 return ref;
256}
257
259 IntegerVariable var,
260 IntegerValue value) {
261 if (!VariableIsPositive(var)) {
262 var = NegationOf(var);
263 value = -value;
264 }
265 literal_to_var_to_value_[literal.Index()][var] = value;
266}
267
269 if (!parameters_.use_implied_bounds()) return true;
270
271 CHECK_EQ(sat_solver_->CurrentDecisionLevel(), 1);
272 tmp_integer_literals_.clear();
273 integer_trail_->AppendNewBounds(&tmp_integer_literals_);
274 for (const IntegerLiteral lit : tmp_integer_literals_) {
275 if (!Add(first_decision, lit)) return false;
276 }
277 return true;
278}
279
280void ElementEncodings::Add(IntegerVariable var,
281 const std::vector<ValueLiteralPair>& encoding,
282 int exactly_one_index) {
283 if (!var_to_index_to_element_encodings_.contains(var)) {
284 element_encoded_variables_.push_back(var);
285 }
286 var_to_index_to_element_encodings_[var][exactly_one_index] = encoding;
287}
288
289const absl::btree_map<int, std::vector<ValueLiteralPair>>&
290ElementEncodings::Get(IntegerVariable var) {
291 const auto& it = var_to_index_to_element_encodings_.find(var);
292 if (it == var_to_index_to_element_encodings_.end()) {
293 return empty_element_encoding_;
294 } else {
295 return it->second;
296 }
297}
298
299const std::vector<IntegerVariable>&
301 return element_encoded_variables_;
302}
303
304// If a variable has a size of 2, it is most likely reduced to an affine
305// expression pointing to a variable with domain [0,1] or [-1,0].
306// If the original variable has been removed from the model, then there are no
307// implied values from any exactly_one constraint to its domain.
308// If we are lucky, one of the literal of the exactly_one constraints, and its
309// negation are used to encode the Boolean variable of the affine.
310//
311// This may fail if exactly_one(l0, l1, l2, l3); l0 and l1 imply x = 0,
312// l2 and l3 imply x = 1. In that case, one must look at the binary
313// implications to find the missing link.
314//
315// TODO(user): Consider removing this once we are more complete in our implied
316// bounds repository. Because if we can reconcile an encoding, then any of the
317// literal in the at most one should imply a value on the boolean view use in
318// the size2 affine.
319std::vector<LiteralValueValue> TryToReconcileEncodings(
320 const AffineExpression& size2_affine, const AffineExpression& affine,
321 absl::Span<const ValueLiteralPair> affine_var_encoding,
322 bool put_affine_left_in_result, IntegerEncoder* integer_encoder) {
323 IntegerVariable binary = size2_affine.var;
324 std::vector<LiteralValueValue> terms;
325 if (!integer_encoder->VariableIsFullyEncoded(binary)) return terms;
326 const std::vector<ValueLiteralPair>& size2_enc =
327 integer_encoder->FullDomainEncoding(binary);
328
329 // TODO(user): I am not sure how this can happen since size2_affine is
330 // supposed to be non-fixed. Maybe we miss some propag. Investigate.
331 if (size2_enc.size() != 2) return terms;
332
333 Literal lit0 = size2_enc[0].literal;
334 IntegerValue value0 = size2_affine.ValueAt(size2_enc[0].value);
335 Literal lit1 = size2_enc[1].literal;
336 IntegerValue value1 = size2_affine.ValueAt(size2_enc[1].value);
337
338 for (const auto& [unused, candidate_literal] : affine_var_encoding) {
339 if (candidate_literal == lit1) {
340 std::swap(lit0, lit1);
341 std::swap(value0, value1);
342 }
343 if (candidate_literal != lit0) continue;
344
345 // Build the decomposition.
346 for (const auto& [value, literal] : affine_var_encoding) {
347 const IntegerValue size_2_value = literal == lit0 ? value0 : value1;
348 const IntegerValue affine_value = affine.ValueAt(value);
349 if (put_affine_left_in_result) {
350 terms.push_back({literal, affine_value, size_2_value});
351 } else {
352 terms.push_back({literal, size_2_value, affine_value});
353 }
354 }
355 break;
356 }
357
358 return terms;
359}
360
361// Specialized case of encoding reconciliation when both variables have a domain
362// of size of 2.
363std::vector<LiteralValueValue> TryToReconcileSize2Encodings(
364 const AffineExpression& left, const AffineExpression& right,
365 IntegerEncoder* integer_encoder) {
366 std::vector<LiteralValueValue> terms;
367 if (!integer_encoder->VariableIsFullyEncoded(left.var) ||
368 !integer_encoder->VariableIsFullyEncoded(right.var)) {
369 return terms;
370 }
371 const std::vector<ValueLiteralPair> left_enc =
372 integer_encoder->FullDomainEncoding(left.var);
373 const std::vector<ValueLiteralPair> right_enc =
374 integer_encoder->FullDomainEncoding(right.var);
375 if (left_enc.size() != 2 || right_enc.size() != 2) {
376 VLOG(2) << "encodings are not fully propagated";
377 return terms;
378 }
379
380 const Literal left_lit0 = left_enc[0].literal;
381 const IntegerValue left_value0 = left.ValueAt(left_enc[0].value);
382 const Literal left_lit1 = left_enc[1].literal;
383 const IntegerValue left_value1 = left.ValueAt(left_enc[1].value);
384
385 const Literal right_lit0 = right_enc[0].literal;
386 const IntegerValue right_value0 = right.ValueAt(right_enc[0].value);
387 const Literal right_lit1 = right_enc[1].literal;
388 const IntegerValue right_value1 = right.ValueAt(right_enc[1].value);
389
390 if (left_lit0 == right_lit0 || left_lit0 == right_lit1.Negated()) {
391 terms.push_back({left_lit0, left_value0, right_value0});
392 terms.push_back({left_lit0.Negated(), left_value1, right_value1});
393 } else if (left_lit0 == right_lit1 || left_lit0 == right_lit0.Negated()) {
394 terms.push_back({left_lit0, left_value0, right_value1});
395 terms.push_back({left_lit0.Negated(), left_value1, right_value0});
396 } else if (left_lit1 == right_lit1 || left_lit1 == right_lit0.Negated()) {
397 terms.push_back({left_lit1.Negated(), left_value0, right_value0});
398 terms.push_back({left_lit1, left_value1, right_value1});
399 } else if (left_lit1 == right_lit0 || left_lit1 == right_lit1.Negated()) {
400 terms.push_back({left_lit1.Negated(), left_value0, right_value1});
401 terms.push_back({left_lit1, left_value1, right_value0});
402 } else {
403 VLOG(3) << "Complex size 2 encoding case, need to scan exactly_ones";
404 }
405
406 return terms;
407}
408
409std::vector<LiteralValueValue> ProductDecomposer::TryToDecompose(
410 const AffineExpression& left, const AffineExpression& right) {
411 if (integer_trail_->IsFixed(left) || integer_trail_->IsFixed(right)) {
412 return {};
413 }
414
415 // Fill in the encodings for the left variable.
416 const absl::btree_map<int, std::vector<ValueLiteralPair>>& left_encodings =
417 element_encodings_->Get(left.var);
418
419 // Fill in the encodings for the right variable.
420 const absl::btree_map<int, std::vector<ValueLiteralPair>>& right_encodings =
421 element_encodings_->Get(right.var);
422
423 std::vector<int> compatible_keys;
424 for (const auto& [index, encoding] : left_encodings) {
425 if (right_encodings.contains(index)) {
426 compatible_keys.push_back(index);
427 }
428 }
429
430 if (compatible_keys.empty()) {
431 if (integer_trail_->InitialVariableDomain(left.var).Size() == 2) {
432 for (const auto& [index, right_encoding] : right_encodings) {
433 const std::vector<LiteralValueValue> result = TryToReconcileEncodings(
434 left, right, right_encoding,
435 /*put_affine_left_in_result=*/false, integer_encoder_);
436 if (!result.empty()) {
437 return result;
438 }
439 }
440 }
441 if (integer_trail_->InitialVariableDomain(right.var).Size() == 2) {
442 for (const auto& [index, left_encoding] : left_encodings) {
443 const std::vector<LiteralValueValue> result = TryToReconcileEncodings(
444 right, left, left_encoding,
445 /*put_affine_left_in_result=*/true, integer_encoder_);
446 if (!result.empty()) {
447 return result;
448 }
449 }
450 }
451 if (integer_trail_->InitialVariableDomain(left.var).Size() == 2 &&
452 integer_trail_->InitialVariableDomain(right.var).Size() == 2) {
453 const std::vector<LiteralValueValue> result =
454 TryToReconcileSize2Encodings(left, right, integer_encoder_);
455 if (!result.empty()) {
456 return result;
457 }
458 }
459 return {};
460 }
461
462 if (compatible_keys.size() > 1) {
463 VLOG(3) << "More than one exactly_one involved in the encoding of the two "
464 "variables";
465 }
466
467 // Select the compatible encoding with the minimum index.
468 const int min_index =
469 *std::min_element(compatible_keys.begin(), compatible_keys.end());
470 // By construction, encodings follow the order of literals in the exactly_one
471 // constraint.
472 const std::vector<ValueLiteralPair>& left_encoding =
473 left_encodings.at(min_index);
474 const std::vector<ValueLiteralPair>& right_encoding =
475 right_encodings.at(min_index);
476 DCHECK_EQ(left_encoding.size(), right_encoding.size());
477
478 // Build decomposition of the product.
479 std::vector<LiteralValueValue> terms;
480 for (int i = 0; i < left_encoding.size(); ++i) {
481 const Literal literal = left_encoding[i].literal;
482 DCHECK_EQ(literal, right_encoding[i].literal);
483 terms.push_back({literal, left.ValueAt(left_encoding[i].value),
484 right.ValueAt(right_encoding[i].value)});
485 }
486
487 return terms;
488}
489
490// TODO(user): Experiment with x * x where constants = 0, x is
491// fully encoded, and the domain is small.
493 const AffineExpression& right,
494 LinearConstraintBuilder* builder) {
495 DCHECK(builder != nullptr);
496 builder->Clear();
497
498 if (integer_trail_->IsFixed(left)) {
499 if (integer_trail_->IsFixed(right)) {
500 builder->AddConstant(integer_trail_->FixedValue(left) *
501 integer_trail_->FixedValue(right));
502 return true;
503 }
504 builder->AddTerm(right, integer_trail_->FixedValue(left));
505 return true;
506 }
507
508 if (integer_trail_->IsFixed(right)) {
509 builder->AddTerm(left, integer_trail_->FixedValue(right));
510 return true;
511 }
512
513 // Linearization is possible if both left and right have the same Boolean
514 // variable.
515 if (PositiveVariable(left.var) == PositiveVariable(right.var) &&
516 integer_trail_->LowerBound(PositiveVariable(left.var)) == 0 &&
517 integer_trail_->UpperBound(PositiveVariable(left.var)) == 1) {
518 const IntegerValue left_coeff =
519 VariableIsPositive(left.var) ? left.coeff : -left.coeff;
520 const IntegerValue right_coeff =
521 VariableIsPositive(right.var) ? right.coeff : -right.coeff;
522 builder->AddTerm(PositiveVariable(left.var),
523 left_coeff * right_coeff + left.constant * right_coeff +
524 left_coeff * right.constant);
525 builder->AddConstant(left.constant * right.constant);
526 return true;
527 }
528
529 const std::vector<LiteralValueValue> decomposition =
530 TryToDecompose(left, right);
531 if (decomposition.empty()) return false;
532
533 IntegerValue min_coefficient = kMaxIntegerValue;
534 for (const LiteralValueValue& term : decomposition) {
535 min_coefficient =
536 std::min(min_coefficient, term.left_value * term.right_value);
537 }
538 for (const LiteralValueValue& term : decomposition) {
539 const IntegerValue coefficient =
540 term.left_value * term.right_value - min_coefficient;
541 if (coefficient == 0) continue;
542 if (!builder->AddLiteralTerm(term.literal, coefficient)) {
543 return false;
544 }
545 }
546 builder->AddConstant(min_coefficient);
547 return true;
548}
549
551 : enabled_(
552 model->GetOrCreate<SatParameters>()->detect_linearized_product() &&
553 model->GetOrCreate<SatParameters>()->linearization_level() > 1),
554 rlt_enabled_(model->GetOrCreate<SatParameters>()->add_rlt_cuts() &&
555 model->GetOrCreate<SatParameters>()->linearization_level() >
556 1),
557 sat_solver_(model->GetOrCreate<SatSolver>()),
558 trail_(model->GetOrCreate<Trail>()),
559 integer_trail_(model->GetOrCreate<IntegerTrail>()),
560 integer_encoder_(model->GetOrCreate<IntegerEncoder>()),
561 shared_stats_(model->GetOrCreate<SharedStatistics>()) {}
562
564 if (!VLOG_IS_ON(1)) return;
565 if (shared_stats_ == nullptr) return;
566 std::vector<std::pair<std::string, int64_t>> stats;
567 stats.push_back(
568 {"product_detector/num_processed_binary", num_processed_binary_});
569 stats.push_back(
570 {"product_detector/num_processed_exactly_one", num_processed_exo_});
571 stats.push_back(
572 {"product_detector/num_processed_ternary", num_processed_ternary_});
573 stats.push_back({"product_detector/num_trail_updates", num_trail_updates_});
574 stats.push_back({"product_detector/num_products", num_products_});
575 stats.push_back({"product_detector/num_conditional_equalities",
576 num_conditional_equalities_});
577 stats.push_back(
578 {"product_detector/num_conditional_zeros", num_conditional_zeros_});
579 stats.push_back({"product_detector/num_int_products", num_int_products_});
580 shared_stats_->AddStats(stats);
581}
582
584 absl::Span<const Literal> ternary_clause) {
585 if (ternary_clause.size() != 3) return;
586 ++num_processed_ternary_;
587
588 if (rlt_enabled_) ProcessTernaryClauseForRLT(ternary_clause);
589 if (!enabled_) return;
590
591 candidates_[GetKey(ternary_clause[0].Index(), ternary_clause[1].Index())]
592 .push_back(ternary_clause[2].Index());
593 candidates_[GetKey(ternary_clause[0].Index(), ternary_clause[2].Index())]
594 .push_back(ternary_clause[1].Index());
595 candidates_[GetKey(ternary_clause[1].Index(), ternary_clause[2].Index())]
596 .push_back(ternary_clause[0].Index());
597
598 // We mark the literal of the ternary clause as seen.
599 // Only a => b with a seen need to be looked at.
600 for (const Literal l : ternary_clause) {
601 if (l.Index() >= seen_.size()) seen_.resize(l.Index() + 1);
602 seen_[l.Index()] = true;
603 }
604}
605
606// If all have view, add to flat representation.
607void ProductDetector::ProcessTernaryClauseForRLT(
608 absl::Span<const Literal> ternary_clause) {
609 const int old_size = ternary_clauses_with_view_.size();
610 for (const Literal l : ternary_clause) {
611 const IntegerVariable var =
612 integer_encoder_->GetLiteralView(Literal(l.Variable(), true));
613 if (var == kNoIntegerVariable || !VariableIsPositive(var)) {
614 ternary_clauses_with_view_.resize(old_size);
615 return;
616 }
617 ternary_clauses_with_view_.push_back(l.IsPositive() ? var
618 : NegationOf(var));
619 }
620}
621
623 absl::Span<const Literal> ternary_exo) {
624 if (ternary_exo.size() != 3) return;
625 ++num_processed_exo_;
626
627 if (rlt_enabled_) ProcessTernaryClauseForRLT(ternary_exo);
628 if (!enabled_) return;
629
630 ProcessNewProduct(ternary_exo[0].Index(), ternary_exo[1].NegatedIndex(),
631 ternary_exo[2].NegatedIndex());
632 ProcessNewProduct(ternary_exo[1].Index(), ternary_exo[0].NegatedIndex(),
633 ternary_exo[2].NegatedIndex());
634 ProcessNewProduct(ternary_exo[2].Index(), ternary_exo[0].NegatedIndex(),
635 ternary_exo[1].NegatedIndex());
636}
637
638// TODO(user): As product are discovered, we could remove entries from our
639// hash maps!
641 absl::Span<const Literal> binary_clause) {
642 if (!enabled_) return;
643 if (binary_clause.size() != 2) return;
644 ++num_processed_binary_;
645 const std::array<LiteralIndex, 2> key =
646 GetKey(binary_clause[0].NegatedIndex(), binary_clause[1].NegatedIndex());
647 std::array<LiteralIndex, 3> ternary;
648 for (const LiteralIndex l : candidates_[key]) {
649 ternary[0] = key[0];
650 ternary[1] = key[1];
651 ternary[2] = l;
652 std::sort(ternary.begin(), ternary.end());
653 const int l_index = ternary[0] == l ? 0 : ternary[1] == l ? 1 : 2;
654 std::bitset<3>& bs = detector_[ternary];
655 if (bs[l_index]) continue;
656 bs[l_index] = true;
657 if (bs[0] && bs[1] && l_index != 2) {
658 ProcessNewProduct(ternary[2], Literal(ternary[0]).NegatedIndex(),
659 Literal(ternary[1]).NegatedIndex());
660 }
661 if (bs[0] && bs[2] && l_index != 1) {
662 ProcessNewProduct(ternary[1], Literal(ternary[0]).NegatedIndex(),
663 Literal(ternary[2]).NegatedIndex());
664 }
665 if (bs[1] && bs[2] && l_index != 0) {
666 ProcessNewProduct(ternary[0], Literal(ternary[1]).NegatedIndex(),
667 Literal(ternary[2]).NegatedIndex());
668 }
669 }
670}
671
673 if (!enabled_) return;
674 for (LiteralIndex a(0); a < seen_.size(); ++a) {
675 if (!seen_[a]) continue;
676 if (trail_->Assignment().LiteralIsAssigned(Literal(a))) continue;
677 const Literal not_a = Literal(a).Negated();
678 for (const Literal b : graph->DirectImplications(Literal(a))) {
679 ProcessBinaryClause({not_a, b}); // a => b;
680 }
681 }
682}
683
685 if (!enabled_) return;
686 if (trail_->CurrentDecisionLevel() != 1) return;
687 ++num_trail_updates_;
688
689 const LiteralWithTrailIndex decision = trail_->Decisions()[0];
690 if (decision.literal.Index() >= seen_.size() ||
691 !seen_[decision.literal.Index()]) {
692 return;
693 }
694 const Literal not_a = decision.literal.Negated();
695 const int current_index = trail_->Index();
696 for (int i = decision.trail_index + 1; i < current_index; ++i) {
697 const Literal b = (*trail_)[i];
698 ProcessBinaryClause({not_a, b});
699 }
700}
701
703 const auto it = products_.find(GetKey(a.Index(), b.Index()));
704 if (it == products_.end()) return kNoLiteralIndex;
705 return it->second;
706}
707
708std::array<LiteralIndex, 2> ProductDetector::GetKey(LiteralIndex a,
709 LiteralIndex b) const {
710 std::array<LiteralIndex, 2> key{a, b};
711 if (key[0] > key[1]) std::swap(key[0], key[1]);
712 return key;
713}
714
715void ProductDetector::ProcessNewProduct(LiteralIndex p, LiteralIndex a,
716 LiteralIndex b) {
717 // If many literal correspond to the same product, we just keep one.
718 ++num_products_;
719 products_[GetKey(a, b)] = p;
720
721 // This is used by ProductIsLinearizable().
722 has_product_.insert(
723 GetKey(Literal(a).IsPositive() ? a : Literal(a).NegatedIndex(),
724 Literal(b).IsPositive() ? b : Literal(b).NegatedIndex()));
725}
726
728 IntegerVariable b) const {
729 if (a == b) return true;
730 if (a == NegationOf(b)) return true;
731
732 // Otherwise, we need both a and b to be expressible as linear expression
733 // involving Booleans whose product is also expressible.
734 if (integer_trail_->InitialVariableDomain(a).Size() != 2) return false;
735 if (integer_trail_->InitialVariableDomain(b).Size() != 2) return false;
736
737 const LiteralIndex la =
738 integer_encoder_->GetAssociatedLiteral(IntegerLiteral::GreaterOrEqual(
739 a, integer_trail_->LevelZeroUpperBound(a)));
740 if (la == kNoLiteralIndex) return false;
741
742 const LiteralIndex lb =
743 integer_encoder_->GetAssociatedLiteral(IntegerLiteral::GreaterOrEqual(
744 b, integer_trail_->LevelZeroUpperBound(b)));
745 if (lb == kNoLiteralIndex) return false;
746
747 // Any product involving la/not(la) * lb/not(lb) can be used.
748 return has_product_.contains(
749 GetKey(Literal(la).IsPositive() ? la : Literal(la).NegatedIndex(),
750 Literal(lb).IsPositive() ? lb : Literal(lb).NegatedIndex()));
751}
752
754 IntegerVariable b) const {
755 const auto it = int_products_.find({a.Index(), PositiveVariable(b)});
756 if (it == int_products_.end()) return kNoIntegerVariable;
757 return VariableIsPositive(b) ? it->second : NegationOf(it->second);
758}
759
760void ProductDetector::ProcessNewProduct(IntegerVariable p, Literal l,
761 IntegerVariable x) {
762 if (!VariableIsPositive(x)) {
763 x = NegationOf(x);
764 p = NegationOf(p);
765 }
766
767 // We only store one product if there are many.
768 ++num_int_products_;
769 int_products_[{l.Index(), x}] = p;
770}
771
773 IntegerVariable y) {
774 ++num_conditional_equalities_;
775 if (x == y) return;
776
777 // We process both possibilities (product = x or product = y).
778 for (int i = 0; i < 2; ++i) {
779 if (!VariableIsPositive(x)) {
780 x = NegationOf(x);
781 y = NegationOf(y);
782 }
783 bool seen = false;
784
785 // TODO(user): Linear scan can be bad if b => X = many other variables.
786 // Hopefully this will not be common.
787 std::vector<IntegerVariable>& others =
788 conditional_equalities_[{l.Index(), x}];
789 for (const IntegerVariable o : others) {
790 if (o == y) {
791 seen = true;
792 break;
793 }
794 }
795
796 if (!seen) {
797 others.push_back(y);
798 if (conditional_zeros_.contains({l.NegatedIndex(), x})) {
799 ProcessNewProduct(/*p=*/x, l, y);
800 }
801 }
802 std::swap(x, y);
803 }
804}
805
807 ++num_conditional_zeros_;
808 p = PositiveVariable(p);
809 auto [_, inserted] = conditional_zeros_.insert({l.Index(), p});
810 if (inserted) {
811 const auto it = conditional_equalities_.find({l.NegatedIndex(), p});
812 if (it != conditional_equalities_.end()) {
813 for (const IntegerVariable x : it->second) {
814 ProcessNewProduct(p, l.Negated(), x);
815 }
816 }
817 }
818}
819
820namespace {
821
822std::pair<IntegerVariable, IntegerVariable> Canonicalize(IntegerVariable a,
823 IntegerVariable b) {
824 if (a < b) return {a, b};
825 return {b, a};
826}
827
828double GetLiteralLpValue(
829 IntegerVariable var,
830 const util_intops::StrongVector<IntegerVariable, double>& lp_values) {
831 return VariableIsPositive(var) ? lp_values[var]
832 : 1.0 - lp_values[PositiveVariable(var)];
833}
834
835} // namespace
836
837void ProductDetector::UpdateRLTMaps(
838 const util_intops::StrongVector<IntegerVariable, double>& lp_values,
839 IntegerVariable var1, double lp1, IntegerVariable var2, double lp2,
840 IntegerVariable bound_var, double bound_lp) {
841 // we have var1 * var2 <= bound_var, and this is only useful if it is better
842 // than the trivial bound <= var1 or <= var2.
843 if (bound_lp > lp1 && bound_lp > lp2) return;
844
845 const auto [it, inserted] =
846 bool_rlt_ubs_.insert({Canonicalize(var1, var2), bound_var});
847
848 // Replace if better.
849 if (!inserted && bound_lp < GetLiteralLpValue(it->second, lp_values)) {
850 it->second = bound_var;
851 }
852
853 // This will increase a RLT cut violation and is a good candidate.
854 if (lp1 * lp2 > bound_lp + 1e-4) {
855 bool_rlt_candidates_[var1].push_back(var2);
856 bool_rlt_candidates_[var2].push_back(var1);
857 }
858}
859
860// TODO(user): limit work if too many ternary.
862 absl::Span<const IntegerVariable> lp_vars,
864 // TODO(user): Maybe we shouldn't reconstruct this every time, but it is hard
865 // in case of multiple lps to make sure we don't use variables not in the lp
866 // otherwise.
867 bool_rlt_ubs_.clear();
868
869 // If we transform a linear constraint to sum positive_coeff * bool <= rhs.
870 // We will list all interesting multiplicative candidate for each variable.
871 bool_rlt_candidates_.clear();
872 const int size = ternary_clauses_with_view_.size();
873 if (size == 0) return;
874
875 is_in_lp_vars_.resize(integer_trail_->NumIntegerVariables().value());
876 for (const IntegerVariable var : lp_vars) is_in_lp_vars_.Set(var);
877
878 for (int i = 0; i < size; i += 3) {
879 const IntegerVariable var1 = ternary_clauses_with_view_[i];
880 const IntegerVariable var2 = ternary_clauses_with_view_[i + 1];
881 const IntegerVariable var3 = ternary_clauses_with_view_[i + 2];
882
883 if (!is_in_lp_vars_[PositiveVariable(var1)]) continue;
884 if (!is_in_lp_vars_[PositiveVariable(var2)]) continue;
885 if (!is_in_lp_vars_[PositiveVariable(var3)]) continue;
886
887 // If we have l1 + l2 + l3 >= 1, then for all (i, j) pair we have
888 // !li * !lj <= lk. We are looking for violation like this.
889 const double lp1 = GetLiteralLpValue(var1, lp_values);
890 const double lp2 = GetLiteralLpValue(var2, lp_values);
891 const double lp3 = GetLiteralLpValue(var3, lp_values);
892
893 UpdateRLTMaps(lp_values, NegationOf(var1), 1.0 - lp1, NegationOf(var2),
894 1.0 - lp2, var3, lp3);
895 UpdateRLTMaps(lp_values, NegationOf(var1), 1.0 - lp1, NegationOf(var3),
896 1.0 - lp3, var2, lp2);
897 UpdateRLTMaps(lp_values, NegationOf(var2), 1.0 - lp2, NegationOf(var3),
898 1.0 - lp3, var1, lp1);
899 }
900
901 // Clear.
902 // TODO(user): Just switch to memclear() when dense.
903 for (const IntegerVariable var : lp_vars) is_in_lp_vars_.ClearBucket(var);
904}
905
906} // namespace sat
907} // namespace operations_research
Domain IntersectionWith(const Domain &domain) const
const std::vector< Literal > & DirectImplications(Literal literal)
Definition clause.cc:3218
void Add(IntegerVariable var, const std::vector< ValueLiteralPair > &encoding, int exactly_one_index)
const absl::btree_map< int, std::vector< ValueLiteralPair > > & Get(IntegerVariable var)
const std::vector< IntegerVariable > & GetElementEncodedVariables() const
bool ProcessIntegerTrail(Literal first_decision)
void AddLiteralImpliesVarEqValue(Literal literal, IntegerVariable var, IntegerValue value)
bool Add(Literal literal, IntegerLiteral integer_literal)
const std::vector< ImpliedBoundEntry > & GetImpliedBounds(IntegerVariable var)
const std::vector< ValueLiteralPair > & FullDomainEncoding(IntegerVariable var) const
Definition integer.cc:146
IntegerVariable GetLiteralView(Literal lit) const
Definition integer.h:265
bool VariableIsFullyEncoded(IntegerVariable var) const
Definition integer.cc:111
ABSL_MUST_USE_RESULT bool AddLiteralTerm(Literal lit, IntegerValue coeff=IntegerValue(1))
void AddTerm(IntegerVariable var, IntegerValue coeff)
LiteralIndex NegatedIndex() const
Definition sat_base.h:93
LiteralIndex Index() const
Definition sat_base.h:92
bool TryToLinearize(const AffineExpression &left, const AffineExpression &right, LinearConstraintBuilder *builder)
std::vector< LiteralValueValue > TryToDecompose(const AffineExpression &left, const AffineExpression &right)
void InitializeBooleanRLTCuts(absl::Span< const IntegerVariable > lp_vars, const util_intops::StrongVector< IntegerVariable, double > &lp_values)
void ProcessTernaryClause(absl::Span< const Literal > ternary_clause)
void ProcessTernaryExactlyOne(absl::Span< const Literal > ternary_exo)
void ProcessImplicationGraph(BinaryImplicationGraph *graph)
LiteralIndex GetProduct(Literal a, Literal b) const
bool ProductIsLinearizable(IntegerVariable a, IntegerVariable b) const
void ProcessConditionalEquality(Literal l, IntegerVariable x, IntegerVariable y)
void ProcessConditionalZero(Literal l, IntegerVariable p)
void ProcessBinaryClause(absl::Span< const Literal > binary_clause)
iterator insert(const_iterator pos, const value_type &x)
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
std::vector< LiteralValueValue > TryToReconcileSize2Encodings(const AffineExpression &left, const AffineExpression &right, IntegerEncoder *integer_encoder)
const LiteralIndex kNoLiteralIndex(-1)
std::vector< IntegerVariable > NegationOf(absl::Span< const IntegerVariable > vars)
Definition integer.cc:52
const IntegerVariable kNoIntegerVariable(-1)
IntegerVariable PositiveVariable(IntegerVariable i)
std::vector< LiteralValueValue > TryToReconcileEncodings(const AffineExpression &size2_affine, const AffineExpression &affine, absl::Span< const ValueLiteralPair > affine_var_encoding, bool put_affine_left_in_result, IntegerEncoder *integer_encoder)
bool VariableIsPositive(IntegerVariable i)
OR-Tools root namespace.
IntegerValue ValueAt(IntegerValue var_value) const
static IntegerLiteral GreaterOrEqual(IntegerVariable i, IntegerValue bound)