OR-Tools  9.6
presolve_util.cc
Go to the documentation of this file.
1 // Copyright 2010-2022 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 <algorithm>
17 #include <array>
18 #include <cstdint>
19 #include <cstdlib>
20 #include <utility>
21 #include <vector>
22 
23 #include "absl/container/flat_hash_map.h"
24 #include "absl/meta/type_traits.h"
25 #include "absl/strings/str_join.h"
26 #include "absl/types/span.h"
27 #include "ortools/base/logging.h"
29 #include "ortools/sat/cp_model.pb.h"
31 #include "ortools/util/bitset.h"
34 
35 namespace operations_research {
36 namespace sat {
37 
38 void DomainDeductions::AddDeduction(int literal_ref, int var, Domain domain) {
39  CHECK_GE(var, 0);
40  const Index index = IndexFromLiteral(literal_ref);
41  if (index >= something_changed_.size()) {
42  something_changed_.Resize(index + 1);
43  enforcement_to_vars_.resize(index.value() + 1);
44  }
45  if (var >= tmp_num_occurrences_.size()) {
46  tmp_num_occurrences_.resize(var + 1, 0);
47  }
48  const auto insert = deductions_.insert({{index, var}, domain});
49  if (insert.second) {
50  // New element.
51  something_changed_.Set(index);
52  enforcement_to_vars_[index].push_back(var);
53  } else {
54  // Existing element.
55  const Domain& old_domain = insert.first->second;
56  if (!old_domain.IsIncludedIn(domain)) {
57  insert.first->second = domain.IntersectionWith(old_domain);
58  something_changed_.Set(index);
59  }
60  }
61 }
62 
63 Domain DomainDeductions::ImpliedDomain(int literal_ref, int var) const {
64  CHECK_GE(var, 0);
65  const Index index = IndexFromLiteral(literal_ref);
66  const auto it = deductions_.find({index, var});
67  if (it == deductions_.end()) return Domain::AllValues();
68  return it->second;
69 }
70 
71 std::vector<std::pair<int, Domain>> DomainDeductions::ProcessClause(
72  absl::Span<const int> clause) {
73  std::vector<std::pair<int, Domain>> result;
74 
75  // We only need to process this clause if something changed since last time.
76  bool abort = true;
77  for (const int ref : clause) {
78  const Index index = IndexFromLiteral(ref);
79  if (index >= something_changed_.size()) return result;
80  if (something_changed_[index]) {
81  abort = false;
82  }
83  }
84  if (abort) return result;
85 
86  // Count for each variable, how many times it appears in the deductions lists.
87  std::vector<int> to_process;
88  std::vector<int> to_clean;
89  for (const int ref : clause) {
90  const Index index = IndexFromLiteral(ref);
91  for (const int var : enforcement_to_vars_[index]) {
92  if (tmp_num_occurrences_[var] == 0) {
93  to_clean.push_back(var);
94  }
95  tmp_num_occurrences_[var]++;
96  if (tmp_num_occurrences_[var] == clause.size()) {
97  to_process.push_back(var);
98  }
99  }
100  }
101 
102  // Clear the counts.
103  for (const int var : to_clean) {
104  tmp_num_occurrences_[var] = 0;
105  }
106 
107  // Compute the domain unions.
108  std::vector<Domain> domains(to_process.size());
109  for (const int ref : clause) {
110  const Index index = IndexFromLiteral(ref);
111  for (int i = 0; i < to_process.size(); ++i) {
112  domains[i] = domains[i].UnionWith(deductions_.at({index, to_process[i]}));
113  }
114  }
115 
116  for (int i = 0; i < to_process.size(); ++i) {
117  result.push_back({to_process[i], std::move(domains[i])});
118  }
119  return result;
120 }
121 
122 namespace {
123 // Helper method for variable substitution. Returns the coefficient of 'var' in
124 // 'proto' and copies other terms in 'terms'.
125 template <typename ProtoWithVarsAndCoeffs>
126 int64_t GetVarCoeffAndCopyOtherTerms(
127  const int var, const ProtoWithVarsAndCoeffs& proto,
128  std::vector<std::pair<int, int64_t>>* terms) {
129  int64_t var_coeff = 0;
130  const int size = proto.vars().size();
131  for (int i = 0; i < size; ++i) {
132  int ref = proto.vars(i);
133  int64_t coeff = proto.coeffs(i);
134  if (!RefIsPositive(ref)) {
135  ref = NegatedRef(ref);
136  coeff = -coeff;
137  }
138 
139  if (ref == var) {
140  // If var appear multiple time, we add its coefficient.
141  var_coeff += coeff;
142  continue;
143  } else {
144  terms->push_back({ref, coeff});
145  }
146  }
147  return var_coeff;
148 }
149 
150 // Helper method for variable substituion. Sorts and merges the terms in 'terms'
151 // and adds them to 'proto'.
152 template <typename ProtoWithVarsAndCoeffs>
153 void SortAndMergeTerms(std::vector<std::pair<int, int64_t>>* terms,
154  ProtoWithVarsAndCoeffs* proto) {
155  proto->clear_vars();
156  proto->clear_coeffs();
157  std::sort(terms->begin(), terms->end());
158  int current_var = 0;
159  int64_t current_coeff = 0;
160  for (const auto& entry : *terms) {
161  CHECK(RefIsPositive(entry.first));
162  if (entry.first == current_var) {
163  current_coeff += entry.second;
164  } else {
165  if (current_coeff != 0) {
166  proto->add_vars(current_var);
167  proto->add_coeffs(current_coeff);
168  }
169  current_var = entry.first;
170  current_coeff = entry.second;
171  }
172  }
173  if (current_coeff != 0) {
174  proto->add_vars(current_var);
175  proto->add_coeffs(current_coeff);
176  }
177 }
178 
179 // Adds all the terms from the var definition constraint with given var
180 // coefficient.
181 void AddTermsFromVarDefinition(const int var, const int64_t var_coeff,
182  const ConstraintProto& definition,
183  std::vector<std::pair<int, int64_t>>* terms) {
184  const int definition_size = definition.linear().vars().size();
185  for (int i = 0; i < definition_size; ++i) {
186  int ref = definition.linear().vars(i);
187  int64_t coeff = definition.linear().coeffs(i);
188  if (!RefIsPositive(ref)) {
189  ref = NegatedRef(ref);
190  coeff = -coeff;
191  }
192 
193  if (ref == var) {
194  continue;
195  } else {
196  terms->push_back({ref, -coeff * var_coeff});
197  }
198  }
199 }
200 } // namespace
201 
202 bool SubstituteVariable(int var, int64_t var_coeff_in_definition,
203  const ConstraintProto& definition,
204  ConstraintProto* ct) {
205  CHECK(RefIsPositive(var));
206  CHECK_EQ(std::abs(var_coeff_in_definition), 1);
207 
208  // Copy all the terms (except the one referring to var).
209  std::vector<std::pair<int, int64_t>> terms;
210  int64_t var_coeff = GetVarCoeffAndCopyOtherTerms(var, ct->linear(), &terms);
211  if (var_coeff == 0) return false;
212 
213  if (var_coeff_in_definition < 0) var_coeff *= -1;
214 
215  AddTermsFromVarDefinition(var, var_coeff, definition, &terms);
216 
217  // The substitution is correct only if we don't loose information here.
218  // But for a constant definition rhs that is always the case.
219  bool exact = false;
220  Domain offset = ReadDomainFromProto(definition.linear());
221  offset = offset.MultiplicationBy(-var_coeff, &exact);
222  CHECK(exact);
223 
224  const Domain rhs = ReadDomainFromProto(ct->linear());
225  FillDomainInProto(rhs.AdditionWith(offset), ct->mutable_linear());
226 
227  SortAndMergeTerms(&terms, ct->mutable_linear());
228  return true;
229 }
230 
232  num_at_most_ones_ = 0;
233  amo_indices_.clear();
234 }
235 
236 void ActivityBoundHelper::AddAtMostOne(absl::Span<const int> amo) {
237  int num_skipped = 0;
238  const int complexity_limit = 50;
239  for (const int literal : amo) {
240  const Index i = IndexFromLiteral(literal);
241  if (i >= amo_indices_.size()) amo_indices_.resize(i + 1);
242  if (amo_indices_[i].size() >= complexity_limit) ++num_skipped;
243  }
244  if (num_skipped + 1 >= amo.size()) return;
245 
246  // Add it.
247  const int unique_index = num_at_most_ones_++;
248  for (const int literal : amo) {
249  const Index i = IndexFromLiteral(literal);
250  if (amo_indices_[i].size() < complexity_limit) {
251  amo_indices_[i].push_back(unique_index);
252  }
253  }
254 }
255 
256 // TODO(user): Add long ones first, or at least the ones of size 2 after.
257 void ActivityBoundHelper::AddAllAtMostOnes(const CpModelProto& proto) {
258  for (const ConstraintProto& ct : proto.constraints()) {
259  const auto type = ct.constraint_case();
260  if (type == ConstraintProto::kAtMostOne) {
261  AddAtMostOne(ct.at_most_one().literals());
262  } else if (type == ConstraintProto::kExactlyOne) {
263  AddAtMostOne(ct.exactly_one().literals());
264  } else if (type == ConstraintProto::kBoolAnd) {
265  if (ct.enforcement_literal().size() == 1) {
266  const int a = ct.enforcement_literal(0);
267  for (const int b : ct.bool_and().literals()) {
268  // a => b same as amo(a, not(b)).
269  AddAtMostOne({a, NegatedRef(b)});
270  }
271  }
272  }
273  }
274 }
275 
276 int64_t ActivityBoundHelper::ComputeActivity(
277  bool compute_min, absl::Span<const std::pair<int, int64_t>> terms,
278  std::vector<std::array<int64_t, 2>>* conditional) {
279  tmp_terms_.clear();
280  tmp_terms_.reserve(terms.size());
281  int64_t offset = 0;
282  for (auto [lit, coeff] : terms) {
283  if (compute_min) coeff = -coeff; // Negate.
284  if (coeff >= 0) {
285  tmp_terms_.push_back({lit, coeff});
286  } else {
287  // l is the same as 1 - (1 - l)
288  tmp_terms_.push_back({NegatedRef(lit), -coeff});
289  offset += coeff;
290  }
291  }
292  const int64_t internal_result =
293  ComputeMaxActivityInternal(tmp_terms_, conditional);
294 
295  // Correct everything.
296  if (conditional != nullptr) {
297  const int num_terms = terms.size();
298  for (int i = 0; i < num_terms; ++i) {
299  if (tmp_terms_[i].first != terms[i].first) {
300  // The true/false meaning is swapped
301  std::swap((*conditional)[i][0], (*conditional)[i][1]);
302  }
303  (*conditional)[i][0] += offset;
304  (*conditional)[i][1] += offset;
305  if (compute_min) {
306  (*conditional)[i][0] = -(*conditional)[i][0];
307  (*conditional)[i][1] = -(*conditional)[i][1];
308  }
309  }
310  }
311  if (compute_min) return -(offset + internal_result);
312  return offset + internal_result;
313 }
314 
315 // Use trivial heuristic for now:
316 // - Sort by decreasing coeff.
317 // - If belong to a chosen part, use it.
318 // - If not, choose biggest part left. TODO(user): compute sum of coeff in part?
319 void ActivityBoundHelper::PartitionIntoAmo(
320  absl::Span<const std::pair<int, int64_t>> terms) {
321  amo_sums_.clear();
322 
323  const int num_terms = terms.size();
324  to_sort_.clear();
325  to_sort_.reserve(num_terms);
326  for (int i = 0; i < num_terms; ++i) {
327  const Index index = IndexFromLiteral(terms[i].first);
328  const int64_t coeff = terms[i].second;
329  if (index < amo_indices_.size()) {
330  for (const int a : amo_indices_[index]) {
331  amo_sums_[a] += coeff;
332  }
333  }
334  to_sort_.push_back({terms[i].second, i});
335  }
336  std::sort(to_sort_.begin(), to_sort_.end(), std::greater<>());
337 
338  int num_parts = 0;
339  partition_.resize(num_terms);
340  used_amo_to_dense_index_.clear();
341  for (int i = 0; i < num_terms; ++i) {
342  const int original_i = to_sort_[i].second;
343  const Index index = IndexFromLiteral(terms[original_i].first);
344  const int64_t coeff = terms[original_i].second;
345  int best = -1;
346  int64_t best_sum = 0;
347  bool done = false;
348  if (index < amo_indices_.size()) {
349  for (const int a : amo_indices_[index]) {
350  const auto it = used_amo_to_dense_index_.find(a);
351  if (it != used_amo_to_dense_index_.end()) {
352  partition_[original_i] = it->second;
353  done = true;
354  break;
355  }
356 
357  const int64_t sum_left = amo_sums_[a];
358  amo_sums_[a] -= coeff;
359  if (sum_left > best_sum) {
360  best_sum = sum_left;
361  best = a;
362  }
363  }
364  }
365  if (done) continue;
366 
367  // New element.
368  if (best == -1) {
369  partition_[original_i] = num_parts++;
370  } else {
371  used_amo_to_dense_index_[best] = num_parts;
372  partition_[original_i] = num_parts;
373  ++num_parts;
374  }
375  }
376  for (const int p : partition_) CHECK_LT(p, num_parts);
377  CHECK_LE(num_parts, num_terms);
378 }
379 
380 // Similar algo as above for this simpler case.
381 std::vector<absl::Span<const int>>
382 ActivityBoundHelper::PartitionLiteralsIntoAmo(absl::Span<const int> literals) {
383  amo_sums_.clear();
384  for (const int ref : literals) {
385  const Index index = IndexFromLiteral(ref);
386  if (index < amo_indices_.size()) {
387  for (const int a : amo_indices_[index]) {
388  amo_sums_[a] += 1;
389  }
390  }
391  }
392 
393  int num_parts = 0;
394  const int num_literals = literals.size();
395  partition_.resize(num_literals);
396  used_amo_to_dense_index_.clear();
397  for (int i = 0; i < literals.size(); ++i) {
398  const Index index = IndexFromLiteral(literals[i]);
399  int best = -1;
400  int64_t best_sum = 0;
401  bool done = false;
402  if (index < amo_indices_.size()) {
403  for (const int a : amo_indices_[index]) {
404  const auto it = used_amo_to_dense_index_.find(a);
405  if (it != used_amo_to_dense_index_.end()) {
406  partition_[i] = it->second;
407  done = true;
408  break;
409  }
410 
411  const int64_t sum_left = amo_sums_[a];
412  amo_sums_[a] -= 1;
413  if (sum_left > best_sum) {
414  best_sum = sum_left;
415  best = a;
416  }
417  }
418  }
419  if (done) continue;
420 
421  // New element.
422  if (best != -1) {
423  used_amo_to_dense_index_[best] = num_parts;
424  }
425  partition_[i] = num_parts++;
426  }
427 
428  // We have the partition, lets construct the spans now.
429  part_starts_.assign(num_parts, 0);
430  part_sizes_.assign(num_parts, 0);
431  part_ends_.assign(num_parts, 0);
432  for (int i = 0; i < num_literals; ++i) {
433  DCHECK_GE(partition_[i], 0);
434  DCHECK_LT(partition_[i], num_parts);
435  part_sizes_[partition_[i]]++;
436  }
437  for (int p = 1; p < num_parts; ++p) {
438  part_starts_[p] = part_ends_[p] = part_sizes_[p - 1] + part_starts_[p - 1];
439  }
440  reordered_literals_.resize(num_literals);
441  for (int i = 0; i < num_literals; ++i) {
442  const int p = partition_[i];
443  reordered_literals_[part_ends_[p]++] = literals[i];
444  }
445  std::vector<absl::Span<const int>> result;
446  for (int p = 0; p < num_parts; ++p) {
447  result.push_back(
448  absl::MakeSpan(&reordered_literals_[part_starts_[p]], part_sizes_[p]));
449  }
450  return result;
451 }
452 
453 bool ActivityBoundHelper::IsAmo(absl::Span<const int> literals) {
454  amo_sums_.clear();
455  for (int i = 0; i < literals.size(); ++i) {
456  bool has_max_size = false;
457  const Index index = IndexFromLiteral(literals[i]);
458  if (index >= amo_indices_.size()) return false;
459  for (const int a : amo_indices_[index]) {
460  if (amo_sums_[a]++ == i) has_max_size = true;
461  }
462  if (!has_max_size) return false;
463  }
464  return true;
465 }
466 
467 int64_t ActivityBoundHelper::ComputeMaxActivityInternal(
468  absl::Span<const std::pair<int, int64_t>> terms,
469  std::vector<std::array<int64_t, 2>>* conditional) {
470  PartitionIntoAmo(terms);
471 
472  // Compute the max coefficient in each partition.
473  const int num_terms = terms.size();
474  max_by_partition_.assign(num_terms, 0);
475  second_max_by_partition_.assign(num_terms, 0);
476  for (int i = 0; i < num_terms; ++i) {
477  const int p = partition_[i];
478  const int64_t coeff = terms[i].second;
479  if (coeff >= max_by_partition_[p]) {
480  second_max_by_partition_[p] = max_by_partition_[p];
481  max_by_partition_[p] = coeff;
482  } else if (coeff > second_max_by_partition_[p]) {
483  second_max_by_partition_[p] = coeff;
484  }
485  }
486 
487  // Once we have this, we can compute bound.
488  int64_t max_activity = 0;
489  for (int p = 0; p < partition_.size(); ++p) {
490  max_activity += max_by_partition_[p];
491  }
492  if (conditional != nullptr) {
493  conditional->resize(num_terms);
494  for (int i = 0; i < num_terms; ++i) {
495  const int64_t coeff = terms[i].second;
496  const int p = partition_[i];
497  const int64_t max_used = max_by_partition_[p];
498 
499  // We have two cases depending if coeff was the maximum in its part or
500  // not.
501  if (coeff == max_used) {
502  // Use the second max.
503  (*conditional)[i][0] =
504  max_activity - max_used + second_max_by_partition_[p];
505  (*conditional)[i][1] = max_activity;
506  } else {
507  // The max is still there, no change at 0 but change for 1.
508  (*conditional)[i][0] = max_activity;
509  (*conditional)[i][1] = max_activity - max_used + coeff;
510  }
511  }
512  }
513  return max_activity;
514 }
515 
517  absl::Span<const int> refs, ConstraintProto* ct,
518  absl::flat_hash_set<int>* literals_at_true) {
519  if (ct->enforcement_literal().empty()) return true;
520 
521  literals_at_true->clear();
522  triggered_amo_.clear();
523  int new_size = 0;
524  for (int i = 0; i < ct->enforcement_literal().size(); ++i) {
525  const int ref = ct->enforcement_literal(i);
526  if (literals_at_true->contains(ref)) continue; // Duplicate.
527  if (literals_at_true->contains(NegatedRef(ref))) return false; // False.
528  literals_at_true->insert(ref);
529 
530  // If a previous enforcement literal implies this one, we can skip it.
531  //
532  // Tricky: We need to do that before appending the amo containing ref in
533  // case an amo contains both ref and not(ref).
534  // TODO(user): Ideally these amo should not be added to this class.
535  const Index negated_index = IndexFromLiteral(NegatedRef(ref));
536  if (negated_index < amo_indices_.size()) {
537  bool skip = false;
538  for (const int a : amo_indices_[negated_index]) {
539  if (triggered_amo_.contains(a)) {
540  skip = true;
541  break;
542  }
543  }
544  if (skip) continue;
545  }
546 
547  const Index index = IndexFromLiteral(ref);
548  if (index < amo_indices_.size()) {
549  for (const int a : amo_indices_[index]) {
550  // If some other literal is at one in this amo, literal must be false,
551  // and so the constraint cannot be enforced.
552  const auto [_, inserted] = triggered_amo_.insert(a);
553  if (!inserted) return false;
554  }
555  }
556 
557  // Keep this enforcement.
558  ct->set_enforcement_literal(new_size++, ref);
559  }
560  ct->mutable_enforcement_literal()->Truncate(new_size);
561 
562  for (const int ref : refs) {
563  // Skip already fixed.
564  if (literals_at_true->contains(ref)) continue;
565  if (literals_at_true->contains(NegatedRef(ref))) continue;
566  for (const int to_test : {ref, NegatedRef(ref)}) {
567  const Index index = IndexFromLiteral(to_test);
568  if (index < amo_indices_.size()) {
569  for (const int a : amo_indices_[index]) {
570  if (triggered_amo_.contains(a)) {
571  // If some other literal is at one in this amo,
572  // literal must be false.
573  if (literals_at_true->contains(to_test)) return false;
574  literals_at_true->insert(NegatedRef(to_test));
575  break;
576  }
577  }
578  }
579  }
580  }
581 
582  return true;
583 }
584 
586  absl::Span<const int> clause) {
587  uint64_t hash = 0;
588  for (const int ref : clause) {
589  const Index index = IndexFromLiteral(ref);
590  while (index >= literal_to_hash_.size()) {
591  // We use random value for a literal hash.
592  literal_to_hash_.push_back(absl::Uniform<uint64_t>(random_));
593  }
594  hash ^= literal_to_hash_[index];
595  }
596 
597  if (c >= clause_to_hash_.size()) clause_to_hash_.resize(c + 1, 0);
598  clause_to_hash_[c] = hash;
599 }
600 
602  absl::Span<const int> literals) {
603  uint64_t hash = 0;
604  for (const int ref : literals) {
605  const Index index = IndexFromLiteral(NegatedRef(ref));
606  while (index >= literal_to_hash_.size()) {
607  // We use random value for a literal hash.
608  literal_to_hash_.push_back(absl::Uniform<uint64_t>(random_));
609  }
610  hash ^= literal_to_hash_[index];
611  }
612  return hash;
613 }
614 
615 } // namespace sat
616 } // namespace operations_research
void resize(size_type new_size)
size_type size() const
void push_back(const value_type &x)
We call domain any subset of Int64 = [kint64min, kint64max].
static Domain AllValues()
Returns the full domain Int64.
bool IsIncludedIn(const Domain &domain) const
Returns true iff D is included in the given domain.
Domain AdditionWith(const Domain &domain) const
Returns {x ∈ Int64, ∃ a ∈ D, ∃ b ∈ domain, x = a + b}.
Domain MultiplicationBy(int64_t coeff, bool *exact=nullptr) const
Returns {x ∈ Int64, ∃ e ∈ D, x = e * coeff}.
Domain IntersectionWith(const Domain &domain) const
Returns the intersection of D and domain.
IntegerType size() const
Definition: bitset.h:758
void Set(IntegerType index)
Definition: bitset.h:792
void Resize(IntegerType size)
Definition: bitset.h:778
std::vector< absl::Span< const int > > PartitionLiteralsIntoAmo(absl::Span< const int > literals)
bool IsAmo(absl::Span< const int > literals)
bool PresolveEnforcement(absl::Span< const int > refs, ConstraintProto *ct, absl::flat_hash_set< int > *literals_at_true)
void AddAtMostOne(absl::Span< const int > amo)
void AddAllAtMostOnes(const CpModelProto &proto)
uint64_t HashOfNegatedLiterals(absl::Span< const int > literals)
void RegisterClause(int c, absl::Span< const int > clause)
std::vector< std::pair< int, Domain > > ProcessClause(absl::Span< const int > clause)
Domain ImpliedDomain(int literal_ref, int var) const
void AddDeduction(int literal_ref, int var, Domain domain)
int64_t b
int64_t a
CpModelProto proto
const Constraint * ct
IntVar * var
Definition: expr_array.cc:1874
int index
int64_t hash
Definition: matrix_utils.cc:63
void swap(IdMap< K, V > &a, IdMap< K, V > &b)
Definition: id_map.h:269
bool RefIsPositive(int ref)
void FillDomainInProto(const Domain &domain, ProtoWithDomain *proto)
Domain ReadDomainFromProto(const ProtoWithDomain &proto)
bool SubstituteVariable(int var, int64_t var_coeff_in_definition, const ConstraintProto &definition, ConstraintProto *ct)
Collection of objects used to extend the Constraint Solver library.
Literal literal
Definition: optimization.cc:88