OR-Tools  9.6
var_domination.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 <stddef.h>
17 
18 #include <algorithm>
19 #include <cstdint>
20 #include <cstdlib>
21 #include <limits>
22 #include <memory>
23 #include <string>
24 #include <utility>
25 #include <vector>
26 
27 #include "absl/strings/str_cat.h"
28 #include "absl/types/span.h"
30 #include "ortools/base/hash.h"
31 #include "ortools/base/logging.h"
32 #include "ortools/base/stl_util.h"
34 #include "ortools/sat/cp_model.pb.h"
36 #include "ortools/sat/integer.h"
41 
42 namespace operations_research {
43 namespace sat {
44 
45 void VarDomination::Reset(int num_variables) {
46  phase_ = 0;
47  num_vars_with_negation_ = 2 * num_variables;
48  partition_ =
49  std::make_unique<SimpleDynamicPartition>(num_vars_with_negation_);
50 
51  can_freely_decrease_.assign(num_vars_with_negation_, true);
52 
53  shared_buffer_.clear();
54  initial_candidates_.assign(num_vars_with_negation_, IntegerVariableSpan());
55 
56  buffer_.clear();
57  dominating_vars_.assign(num_vars_with_negation_, IntegerVariableSpan());
58 
59  ct_index_for_signature_ = 0;
60  block_down_signatures_.assign(num_vars_with_negation_, 0);
61 }
62 
63 void VarDomination::RefinePartition(std::vector<int>* vars) {
64  if (vars->empty()) return;
65  partition_->Refine(*vars);
66  for (int& var : *vars) {
67  const IntegerVariable wrapped(var);
68  can_freely_decrease_[wrapped] = false;
69  can_freely_decrease_[NegationOf(wrapped)] = false;
70  var = NegationOf(wrapped).value();
71  }
72  partition_->Refine(*vars);
73 }
74 
75 void VarDomination::CanOnlyDominateEachOther(absl::Span<const int> refs) {
76  if (phase_ != 0) return;
77  tmp_vars_.clear();
78  for (const int ref : refs) {
79  tmp_vars_.push_back(RefToIntegerVariable(ref).value());
80  }
81  RefinePartition(&tmp_vars_);
82  tmp_vars_.clear();
83 }
84 
85 void VarDomination::ActivityShouldNotChange(absl::Span<const int> refs,
86  absl::Span<const int64_t> coeffs) {
87  if (phase_ != 0) return;
88  FillTempRanks(/*reverse_references=*/false, /*enforcements=*/{}, refs,
89  coeffs);
90  tmp_vars_.clear();
91  for (int i = 0; i < tmp_ranks_.size(); ++i) {
92  if (i > 0 && tmp_ranks_[i].rank != tmp_ranks_[i - 1].rank) {
93  RefinePartition(&tmp_vars_);
94  tmp_vars_.clear();
95  }
96  tmp_vars_.push_back(tmp_ranks_[i].var.value());
97  }
98  RefinePartition(&tmp_vars_);
99  tmp_vars_.clear();
100 }
101 
102 // This correspond to a lower bounded constraint.
103 void VarDomination::ProcessTempRanks() {
104  if (phase_ == 0) {
105  // We actually "split" tmp_ranks_ according to the current partition and
106  // process each resulting list independently for a faster algo.
107  ++ct_index_for_signature_;
108  for (IntegerVariableWithRank& entry : tmp_ranks_) {
109  can_freely_decrease_[entry.var] = false;
110  block_down_signatures_[entry.var] |= uint64_t{1}
111  << (ct_index_for_signature_ % 64);
112  entry.part = partition_->PartOf(entry.var.value());
113  }
114  std::stable_sort(
115  tmp_ranks_.begin(), tmp_ranks_.end(),
116  [](const IntegerVariableWithRank& a, const IntegerVariableWithRank& b) {
117  return a.part < b.part;
118  });
119  int start = 0;
120  for (int i = 1; i < tmp_ranks_.size(); ++i) {
121  if (tmp_ranks_[i].part != tmp_ranks_[start].part) {
122  Initialize({&tmp_ranks_[start], static_cast<size_t>(i - start)});
123  start = i;
124  }
125  }
126  if (start < tmp_ranks_.size()) {
127  Initialize({&tmp_ranks_[start], tmp_ranks_.size() - start});
128  }
129  } else if (phase_ == 1) {
130  FilterUsingTempRanks();
131  } else {
132  // This is only used for debugging, and we shouldn't reach here in prod.
133  CheckUsingTempRanks();
134  }
135 }
136 
138  absl::Span<const int> enforcements, absl::Span<const int> refs,
139  absl::Span<const int64_t> coeffs) {
140  FillTempRanks(/*reverse_references=*/false, enforcements, refs, coeffs);
141  ProcessTempRanks();
142 }
143 
145  absl::Span<const int> enforcements, absl::Span<const int> refs,
146  absl::Span<const int64_t> coeffs) {
147  FillTempRanks(/*reverse_references=*/true, enforcements, refs, coeffs);
148  ProcessTempRanks();
149 }
150 
151 void VarDomination::MakeRankEqualToStartOfPart(
152  absl::Span<IntegerVariableWithRank> span) {
153  const int size = span.size();
154  int start = 0;
155  int previous_value = 0;
156  for (int i = 0; i < size; ++i) {
157  const int64_t value = span[i].rank;
158  if (value != previous_value) {
159  previous_value = value;
160  start = i;
161  }
162  span[i].rank = start;
163  }
164 }
165 
166 void VarDomination::Initialize(absl::Span<IntegerVariableWithRank> span) {
167  // The rank can be wrong and need to be recomputed because of how we split
168  // tmp_ranks_ into spans.
169  MakeRankEqualToStartOfPart(span);
170 
171  const int future_start = shared_buffer_.size();
172  int first_start = -1;
173 
174  // This is mainly to avoid corner case and hopefully, it should be big enough
175  // to not matter too much.
176  const int kSizeThreshold = 1000;
177  const int size = span.size();
178  for (int i = std::max(0, size - kSizeThreshold); i < size; ++i) {
179  const IntegerVariableWithRank entry = span[i];
180  const int num_candidates = size - entry.rank;
181  if (num_candidates >= kSizeThreshold) continue;
182 
183  // Compute size to beat.
184  int size_threshold = kSizeThreshold;
185 
186  // Take into account the current partition size.
187  const int var_part = partition_->PartOf(entry.var.value());
188  const int part_size = partition_->SizeOfPart(var_part);
189  size_threshold = std::min(size_threshold, part_size);
190 
191  // Take into account our current best candidate if there is one.
192  const int current_num_candidates = initial_candidates_[entry.var].size;
193  if (current_num_candidates != 0) {
194  size_threshold = std::min(size_threshold, current_num_candidates);
195  }
196 
197  if (num_candidates < size_threshold) {
198  if (first_start == -1) first_start = entry.rank;
199  initial_candidates_[entry.var] = {
200  future_start - first_start + static_cast<int>(entry.rank),
201  num_candidates};
202  }
203  }
204 
205  // Only store what is necessary.
206  if (first_start == -1) return;
207  for (int i = first_start; i < size; ++i) {
208  shared_buffer_.push_back(span[i].var);
209  }
210 }
211 
212 // TODO(user): Use more heuristics to not miss as much dominance relation when
213 // we crop initial lists.
215  CHECK_EQ(phase_, 0);
216  phase_ = 1;
217 
218  // Some initial lists ar too long and will be cropped to this size.
219  // We will handle them slightly differently.
220  //
221  // TODO(user): Tune the initial size, 50 might be a bit large, since our
222  // complexity is borned by this number times the number of entries in the
223  // constraints. Still we should in most situation be a lot lower than that.
224  const int kMaxInitialSize = 50;
225  std::vector<IntegerVariable> cropped_lists;
226  absl::StrongVector<IntegerVariable, bool> is_cropped(num_vars_with_negation_,
227  false);
228 
229  // Fill the initial domination candidates.
230  std::vector<int> buffer;
231  const auto elements_by_part = partition_->GetParts(&buffer);
232  for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
233  if (can_freely_decrease_[var]) continue;
234  const int part = partition_->PartOf(var.value());
235  const int part_size = partition_->SizeOfPart(part);
236 
237  const int start = buffer_.size();
238  int new_size = 0;
239 
240  const uint64_t var_sig = block_down_signatures_[var];
241  const uint64_t not_var_sig = block_down_signatures_[NegationOf(var)];
242  const int stored_size = initial_candidates_[var].size;
243  if (stored_size == 0 || part_size < stored_size) {
244  // We start with the partition part.
245  // Note that all constraint will be filtered again in the second pass.
246  int num_tested = 0;
247  for (const int value : elements_by_part[part]) {
248  const IntegerVariable c = IntegerVariable(value);
249 
250  // This is to limit the complexity to 1k * num_vars. We fill the list
251  // with dummy node so that the heuristic below will fill it with
252  // potential transpose candidates.
253  if (++num_tested > 1000) {
254  is_cropped[var] = true;
255  cropped_lists.push_back(var);
256  int extra = new_size;
257  while (extra < kMaxInitialSize) {
258  ++extra;
259  buffer_.push_back(kNoIntegerVariable);
260  }
261  break;
262  }
263  if (PositiveVariable(c) == PositiveVariable(var)) continue;
264  if (can_freely_decrease_[NegationOf(c)]) continue;
265  if (var_sig & ~block_down_signatures_[c]) continue; // !included.
266  if (block_down_signatures_[NegationOf(c)] & ~not_var_sig) continue;
267  ++new_size;
268  buffer_.push_back(c);
269 
270  // We do not want too many candidates per variables.
271  // TODO(user): randomize?
272  if (new_size > kMaxInitialSize) {
273  is_cropped[var] = true;
274  cropped_lists.push_back(var);
275  break;
276  }
277  }
278  } else {
279  // Copy the one that are in the same partition_ part.
280  //
281  // TODO(user): This can be too long maybe? even if we have list of at
282  // most 1000 at this point, see InitializeUsingTempRanks().
283  for (const IntegerVariable c : InitialDominatingCandidates(var)) {
284  if (PositiveVariable(c) == PositiveVariable(var)) continue;
285  if (can_freely_decrease_[NegationOf(c)]) continue;
286  if (partition_->PartOf(c.value()) != part) continue;
287  if (var_sig & ~block_down_signatures_[c]) continue; // !included.
288  if (block_down_signatures_[NegationOf(c)] & ~not_var_sig) continue;
289  ++new_size;
290  buffer_.push_back(c);
291 
292  // We do not want too many candidates per variables.
293  // TODO(user): randomize?
294  if (new_size > kMaxInitialSize) {
295  is_cropped[var] = true;
296  cropped_lists.push_back(var);
297  break;
298  }
299  }
300  }
301 
302  dominating_vars_[var] = {start, new_size};
303  }
304 
305  // Heuristic: To try not to remove domination relations corresponding to short
306  // lists during transposition (see EndSecondPhase()), we fill half of the
307  // cropped list with the transpose of the short list relations. This helps
308  // finding more relation in the presence of cropped lists.
309  for (const IntegerVariable var : cropped_lists) {
310  if (kMaxInitialSize / 2 < dominating_vars_[var].size) {
311  dominating_vars_[var].size = kMaxInitialSize / 2; // Restrict.
312  }
313  }
314  for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
315  for (const IntegerVariable dom : DominatingVariables(var)) {
316  if (!is_cropped[NegationOf(dom)]) continue;
317  IntegerVariableSpan& s = dominating_vars_[NegationOf(dom)];
318  if (s.size >= kMaxInitialSize) continue;
319  buffer_[s.start + s.size++] = NegationOf(var);
320  }
321  }
322 
323  // Remove any duplicates.
324  //
325  // TODO(user): Maybe we should do that with all lists in case the
326  // input function are called with duplicates too.
327  for (const IntegerVariable var : cropped_lists) {
328  if (!is_cropped[var]) continue;
329  IntegerVariableSpan& s = dominating_vars_[var];
330  std::sort(&buffer_[s.start], &buffer_[s.start + s.size]);
331  const auto p = std::unique(&buffer_[s.start], &buffer_[s.start + s.size]);
332  s.size = p - &buffer_[s.start];
333  }
334 
335  // We no longer need the first phase memory.
336  VLOG(1) << "Num initial list that where cropped: " << cropped_lists.size();
337  VLOG(1) << "Shared buffer size: " << shared_buffer_.size();
338  VLOG(1) << "Buffer size: " << buffer_.size();
339  gtl::STLClearObject(&initial_candidates_);
340  gtl::STLClearObject(&shared_buffer_);
341 
342  // A second phase is only needed if there are some potential dominance
343  // relations.
344  return !buffer_.empty();
345 }
346 
348  CHECK_EQ(phase_, 1);
349  phase_ = 2;
350 
351  // Perform intersection with transpose.
352  shared_buffer_.clear();
353  initial_candidates_.assign(num_vars_with_negation_, IntegerVariableSpan());
354 
355  // Pass 1: count.
356  for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
357  for (const IntegerVariable dom : DominatingVariables(var)) {
358  ++initial_candidates_[NegationOf(dom)].size;
359  }
360  }
361 
362  // Pass 2: compute starts.
363  int start = 0;
364  for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
365  initial_candidates_[var].start = start;
366  start += initial_candidates_[var].size;
367  initial_candidates_[var].size = 0;
368  }
369  shared_buffer_.resize(start);
370 
371  // Pass 3: transpose.
372  for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
373  for (const IntegerVariable dom : DominatingVariables(var)) {
374  IntegerVariableSpan& span = initial_candidates_[NegationOf(dom)];
375  shared_buffer_[span.start + span.size++] = NegationOf(var);
376  }
377  }
378 
379  // Pass 4: intersect.
380  int num_removed = 0;
381  tmp_var_to_rank_.resize(num_vars_with_negation_, -1);
382  for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
383  for (const IntegerVariable dom : InitialDominatingCandidates(var)) {
384  tmp_var_to_rank_[dom] = 1;
385  }
386 
387  int new_size = 0;
388  IntegerVariableSpan& span = dominating_vars_[var];
389  for (const IntegerVariable dom : DominatingVariables(var)) {
390  if (tmp_var_to_rank_[dom] != 1) {
391  ++num_removed;
392  continue;
393  }
394  buffer_[span.start + new_size++] = dom;
395  }
396  span.size = new_size;
397 
398  for (const IntegerVariable dom : InitialDominatingCandidates(var)) {
399  tmp_var_to_rank_[dom] = -1;
400  }
401  }
402 
403  VLOG(1) << "Transpose removed " << num_removed;
404  gtl::STLClearObject(&initial_candidates_);
405  gtl::STLClearObject(&shared_buffer_);
406 }
407 
408 void VarDomination::FillTempRanks(bool reverse_references,
409  absl::Span<const int> enforcements,
410  absl::Span<const int> refs,
411  absl::Span<const int64_t> coeffs) {
412  tmp_ranks_.clear();
413  if (coeffs.empty()) {
414  // Simple case: all coefficients are assumed to be the same.
415  for (const int ref : refs) {
416  const IntegerVariable var =
417  RefToIntegerVariable(reverse_references ? NegatedRef(ref) : ref);
418  tmp_ranks_.push_back({var, 0, 0});
419  }
420  } else {
421  // Complex case: different coefficients.
422  for (int i = 0; i < refs.size(); ++i) {
423  if (coeffs[i] == 0) continue;
424  const IntegerVariable var = RefToIntegerVariable(
425  reverse_references ? NegatedRef(refs[i]) : refs[i]);
426  if (coeffs[i] > 0) {
427  tmp_ranks_.push_back({var, 0, coeffs[i]});
428  } else {
429  tmp_ranks_.push_back({NegationOf(var), 0, -coeffs[i]});
430  }
431  }
432  std::sort(tmp_ranks_.begin(), tmp_ranks_.end());
433  MakeRankEqualToStartOfPart({&tmp_ranks_[0], tmp_ranks_.size()});
434  }
435 
436  // Add the enforcement last with a new rank. We add their negation since
437  // we want the activity to not decrease, and we want to allow any
438  // enforcement-- to dominate a variable in the constraint.
439  const int enforcement_rank = tmp_ranks_.size();
440  for (const int ref : enforcements) {
441  tmp_ranks_.push_back(
442  {RefToIntegerVariable(NegatedRef(ref)), 0, enforcement_rank});
443  }
444 }
445 
446 // We take the intersection of the current dominating candidate with the
447 // restriction imposed by the current content of tmp_ranks_.
448 void VarDomination::FilterUsingTempRanks() {
449  // Expand ranks in temp vector.
450  tmp_var_to_rank_.resize(num_vars_with_negation_, -1);
451  for (const IntegerVariableWithRank entry : tmp_ranks_) {
452  tmp_var_to_rank_[entry.var] = entry.rank;
453  }
454 
455  // The activity of the variable in tmp_rank must not decrease.
456  for (const IntegerVariableWithRank entry : tmp_ranks_) {
457  // The only variables that can be paired with a var-- in the constraints are
458  // the var++ in the constraints with the same rank or higher.
459  //
460  // Note that we only filter the var-- domination lists here, we do not
461  // remove the var-- appearing in all the lists corresponding to wrong var++.
462  // This is left to the transpose operation in EndSecondPhase().
463  {
464  IntegerVariableSpan& span = dominating_vars_[entry.var];
465  if (span.size == 0) continue;
466  int new_size = 0;
467  for (const IntegerVariable candidate : DominatingVariables(entry.var)) {
468  if (tmp_var_to_rank_[candidate] < entry.rank) continue;
469  buffer_[span.start + new_size++] = candidate;
470  }
471  span.size = new_size;
472  }
473  }
474 
475  // Reset temporary vector to all -1.
476  for (const IntegerVariableWithRank entry : tmp_ranks_) {
477  tmp_var_to_rank_[entry.var] = -1;
478  }
479 }
480 
481 // Slow: This is for debugging only.
482 void VarDomination::CheckUsingTempRanks() {
483  tmp_var_to_rank_.resize(num_vars_with_negation_, -1);
484  for (const IntegerVariableWithRank entry : tmp_ranks_) {
485  tmp_var_to_rank_[entry.var] = entry.rank;
486  }
487 
488  // The activity of the variable in tmp_rank must not decrease.
489  for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
490  const int var_rank = tmp_var_to_rank_[var];
491  const int negated_var_rank = tmp_var_to_rank_[NegationOf(var)];
492  for (const IntegerVariable dom : DominatingVariables(var)) {
493  CHECK(!can_freely_decrease_[NegationOf(dom)]);
494 
495  // Doing X--, Y++ is compatible if the rank[X] <= rank[Y]. But we also
496  // need to check if doing Not(Y)-- is compatible with Not(X)++.
497  CHECK_LE(var_rank, tmp_var_to_rank_[dom]);
498  CHECK_LE(tmp_var_to_rank_[NegationOf(dom)], negated_var_rank);
499  }
500  }
501 
502  for (const IntegerVariableWithRank entry : tmp_ranks_) {
503  tmp_var_to_rank_[entry.var] = -1;
504  }
505 }
506 
507 bool VarDomination::CanFreelyDecrease(int ref) const {
509 }
510 
511 bool VarDomination::CanFreelyDecrease(IntegerVariable var) const {
512  return can_freely_decrease_[var];
513 }
514 
515 absl::Span<const IntegerVariable> VarDomination::InitialDominatingCandidates(
516  IntegerVariable var) const {
517  const IntegerVariableSpan span = initial_candidates_[var];
518  if (span.size == 0) return absl::Span<const IntegerVariable>();
519  return absl::Span<const IntegerVariable>(&shared_buffer_[span.start],
520  span.size);
521 }
522 
523 absl::Span<const IntegerVariable> VarDomination::DominatingVariables(
524  int ref) const {
526 }
527 
528 absl::Span<const IntegerVariable> VarDomination::DominatingVariables(
529  IntegerVariable var) const {
530  const IntegerVariableSpan span = dominating_vars_[var];
531  if (span.size == 0) return absl::Span<const IntegerVariable>();
532  return absl::Span<const IntegerVariable>(&buffer_[span.start], span.size);
533 }
534 
535 std::string VarDomination::DominationDebugString(IntegerVariable var) const {
536  const int ref = IntegerVariableToRef(var);
537  std::string result =
538  absl::StrCat(PositiveRef(ref), RefIsPositive(ref) ? "--" : "++", " : ");
539  for (const IntegerVariable dom : DominatingVariables(var)) {
540  const int dom_ref = IntegerVariableToRef(dom);
541  absl::StrAppend(&result, PositiveRef(dom_ref),
542  RefIsPositive(dom_ref) ? "++" : "--", " ");
543  }
544  return result;
545 }
546 
547 // TODO(user): No need to set locking_ct_index_[var] if num_locks_[var] > 1
548 void DualBoundStrengthening::CannotDecrease(absl::Span<const int> refs,
549  int ct_index) {
550  for (const int ref : refs) {
551  const IntegerVariable var = RefToIntegerVariable(ref);
552  can_freely_decrease_until_[var] = kMaxIntegerValue;
553  num_locks_[var]++;
554  locking_ct_index_[var] = ct_index;
555  }
556 }
557 
558 void DualBoundStrengthening::CannotIncrease(absl::Span<const int> refs,
559  int ct_index) {
560  for (const int ref : refs) {
561  const IntegerVariable var = RefToIntegerVariable(ref);
562  can_freely_decrease_until_[NegationOf(var)] = kMaxIntegerValue;
563  num_locks_[NegationOf(var)]++;
564  locking_ct_index_[NegationOf(var)] = ct_index;
565  }
566 }
567 
568 void DualBoundStrengthening::CannotMove(absl::Span<const int> refs,
569  int ct_index) {
570  for (const int ref : refs) {
571  const IntegerVariable var = RefToIntegerVariable(ref);
572  can_freely_decrease_until_[var] = kMaxIntegerValue;
573  can_freely_decrease_until_[NegationOf(var)] = kMaxIntegerValue;
574  num_locks_[var]++;
575  num_locks_[NegationOf(var)]++;
576  locking_ct_index_[var] = ct_index;
577  locking_ct_index_[NegationOf(var)] = ct_index;
578  }
579 }
580 
581 template <typename LinearProto>
583  bool is_objective, const PresolveContext& context,
584  const LinearProto& linear, int64_t min_activity, int64_t max_activity,
585  int ct_index) {
586  const int64_t lb_limit = linear.domain(linear.domain_size() - 2);
587  const int64_t ub_limit = linear.domain(1);
588  const int num_terms = linear.vars_size();
589  for (int i = 0; i < num_terms; ++i) {
590  int ref = linear.vars(i);
591  int64_t coeff = linear.coeffs(i);
592  if (coeff < 0) {
593  ref = NegatedRef(ref);
594  coeff = -coeff;
595  }
596 
597  const int64_t min_term = coeff * context.MinOf(ref);
598  const int64_t max_term = coeff * context.MaxOf(ref);
599  const int64_t term_diff = max_term - min_term;
600  const IntegerVariable var = RefToIntegerVariable(ref);
601 
602  // lb side.
603  if (min_activity < lb_limit) {
604  num_locks_[var]++;
605  locking_ct_index_[var] = ct_index;
606  if (min_activity + term_diff < lb_limit) {
607  can_freely_decrease_until_[var] = kMaxIntegerValue;
608  } else {
609  const IntegerValue slack(lb_limit - min_activity);
610  const IntegerValue var_diff =
611  CeilRatio(IntegerValue(slack), IntegerValue(coeff));
612  can_freely_decrease_until_[var] =
613  std::max(can_freely_decrease_until_[var],
614  IntegerValue(context.MinOf(ref)) + var_diff);
615  }
616  }
617 
618  if (is_objective) {
619  // We never want to increase the objective value. Note that if the
620  // objective is lower bounded, we checked that on the lb side above.
621  num_locks_[NegationOf(var)]++;
622  can_freely_decrease_until_[NegationOf(var)] = kMaxIntegerValue;
623  continue;
624  }
625 
626  // ub side.
627  if (max_activity > ub_limit) {
628  num_locks_[NegationOf(var)]++;
629  locking_ct_index_[NegationOf(var)] = ct_index;
630  if (max_activity - term_diff > ub_limit) {
631  can_freely_decrease_until_[NegationOf(var)] = kMaxIntegerValue;
632  } else {
633  const IntegerValue slack(max_activity - ub_limit);
634  const IntegerValue var_diff =
635  CeilRatio(IntegerValue(slack), IntegerValue(coeff));
636  can_freely_decrease_until_[NegationOf(var)] =
637  std::max(can_freely_decrease_until_[NegationOf(var)],
638  -IntegerValue(context.MaxOf(ref)) + var_diff);
639  }
640  }
641  }
642 }
643 
644 namespace {
645 
646 // This is used to detect if two linear constraint are equivalent if the literal
647 // ref is mapped to another value. We fill a vector that will only be equal
648 // to another such vector if the two constraint differ only there.
649 void TransformLinearWithSpecialBoolean(const ConstraintProto& ct, int ref,
650  std::vector<int64_t>* output) {
651  DCHECK_EQ(ct.constraint_case(), ConstraintProto::kLinear);
652  output->clear();
653 
654  // Deal with enforcement.
655  // We only detect NegatedRef() here.
656  if (!ct.enforcement_literal().empty()) {
657  output->push_back(ct.enforcement_literal().size());
658  for (const int literal : ct.enforcement_literal()) {
659  if (literal == NegatedRef(ref)) {
660  output->push_back(std::numeric_limits<int32_t>::max()); // Sentinel
661  } else {
662  output->push_back(literal);
663  }
664  }
665  }
666 
667  // Deal with linear part.
668  // We look for both literal and not(literal) here.
669  int64_t offset = 0;
670  output->push_back(ct.linear().vars().size());
671  for (int i = 0; i < ct.linear().vars().size(); ++i) {
672  const int v = ct.linear().vars(i);
673  const int64_t c = ct.linear().coeffs(i);
674  if (v == ref) {
675  output->push_back(std::numeric_limits<int32_t>::max()); // Sentinel
676  output->push_back(c);
677  } else if (v == NegatedRef(ref)) {
678  // c * v = -c * (1 - v) + c
679  output->push_back(std::numeric_limits<int32_t>::max()); // Sentinel
680  output->push_back(-c);
681  offset += c;
682  } else {
683  output->push_back(v);
684  output->push_back(c);
685  }
686  }
687 
688  // Domain.
689  for (const int64_t value : ct.linear().domain()) {
690  output->push_back(value - offset);
691  }
692 }
693 
694 } // namespace
695 
697  num_deleted_constraints_ = 0;
698  const CpModelProto& cp_model = *context->working_model;
699  const int num_vars = cp_model.variables_size();
700  int64_t num_fixed_vars = 0;
701  for (int var = 0; var < num_vars; ++var) {
702  if (context->IsFixed(var)) continue;
703 
704  // Fix to lb?
705  const int64_t lb = context->MinOf(var);
706  const int64_t ub_limit = std::max(lb, CanFreelyDecreaseUntil(var));
707  if (ub_limit == lb) {
708  ++num_fixed_vars;
709  CHECK(context->IntersectDomainWith(var, Domain(lb)));
710  continue;
711  }
712 
713  // Fix to ub?
714  const int64_t ub = context->MaxOf(var);
715  const int64_t lb_limit =
717  if (lb_limit == ub) {
718  ++num_fixed_vars;
719  CHECK(context->IntersectDomainWith(var, Domain(ub)));
720  continue;
721  }
722 
723  // Here we can fix to any value in [ub_limit, lb_limit] that is compatible
724  // with the current domain. We prefer zero or the lowest possible magnitude.
725  if (lb_limit > ub_limit) {
726  const Domain domain =
727  context->DomainOf(var).IntersectionWith(Domain(ub_limit, lb_limit));
728  if (!domain.IsEmpty()) {
729  int64_t value = domain.Contains(0) ? 0 : domain.Min();
730  if (value != 0) {
731  for (const int64_t bound : domain.FlattenedIntervals()) {
732  if (std::abs(bound) < std::abs(value)) value = bound;
733  }
734  }
735  context->UpdateRuleStats("dual: fix variable with multiple choices");
736  CHECK(context->IntersectDomainWith(var, Domain(value)));
737  continue;
738  }
739  }
740 
741  // Here we can reduce the domain, but we must be careful when the domain
742  // has holes.
743  if (lb_limit > lb || ub_limit < ub) {
744  const int64_t new_ub =
745  ub_limit < ub
746  ? context->DomainOf(var)
747  .IntersectionWith(
749  .Min()
750  : ub;
751  const int64_t new_lb =
752  lb_limit > lb
753  ? context->DomainOf(var)
754  .IntersectionWith(
756  .Max()
757  : lb;
758  context->UpdateRuleStats("dual: reduced domain");
759  CHECK(context->IntersectDomainWith(var, Domain(new_lb, new_ub)));
760  }
761  }
762  if (num_fixed_vars > 0) {
763  context->UpdateRuleStats("dual: fix variable", num_fixed_vars);
764  }
765 
766  // For detecting near-duplicate constraint that can be made equivalent.
767  // hash -> (ct_index, modified ref).
768  absl::flat_hash_map<uint64_t, std::pair<int, int>> equiv_modified_constraints;
769  std::vector<int64_t> temp_data;
770  std::vector<int64_t> other_temp_data;
771  std::string s;
772 
773  // If there is only one blocking constraint, we can simplify the problem in
774  // a few situation.
775  //
776  // TODO(user): Cover all the cases.
777  int64_t work_done = 0;
778  const int64_t work_limit = static_cast<int64_t>(1e9);
779  std::vector<bool> processed(num_vars, false);
780  int64_t num_bool_in_near_duplicate_ct = 0;
781  for (IntegerVariable var(0); var < num_locks_.size(); ++var) {
782  const int ref = VarDomination::IntegerVariableToRef(var);
783  const int positive_ref = PositiveRef(ref);
784  if (processed[positive_ref]) continue;
785  if (context->IsFixed(positive_ref)) continue;
786  if (context->VariableIsNotUsedAnymore(positive_ref)) continue;
787  if (context->VariableWasRemoved(positive_ref)) continue;
788 
789  if (num_locks_[var] != 1) continue;
790  if (locking_ct_index_[var] == -1) {
791  context->UpdateRuleStats(
792  "TODO dual: only one unspecified blocking constraint?");
793  continue;
794  }
795 
796  const int ct_index = locking_ct_index_[var];
797  const ConstraintProto& ct = context->working_model->constraints(ct_index);
798  if (ct.constraint_case() == ConstraintProto::CONSTRAINT_NOT_SET) {
799  // TODO(user): Fix variable right away rather than waiting for next call.
800  continue;
801  }
802  if (ct.constraint_case() == ConstraintProto::kAtMostOne) {
803  context->UpdateRuleStats("TODO dual: tighten at most one");
804  continue;
805  }
806 
807  if (ct.constraint_case() != ConstraintProto::kBoolAnd) {
808  // If we have an enforcement literal then we can always add the
809  // implication "not enforced" => var at its lower bound.
810  // If we also had enforced => fixed var, then var is in affine relation
811  // with the enforced literal and we can remove one variable.
812  //
813  // TODO(user): We can also deal with more than one enforcement.
814  if (ct.enforcement_literal().size() == 1 &&
815  PositiveRef(ct.enforcement_literal(0)) != positive_ref) {
816  const int enf = ct.enforcement_literal(0);
817  const int64_t bound = RefIsPositive(ref) ? context->MinOf(positive_ref)
818  : context->MaxOf(positive_ref);
819  const Domain implied =
820  context->DomainOf(positive_ref)
821  .IntersectionWith(
822  context->deductions.ImpliedDomain(enf, positive_ref));
823  if (implied.IsEmpty()) {
824  context->UpdateRuleStats("dual: fix variable");
825  if (!context->SetLiteralToFalse(enf)) return false;
826  if (!context->IntersectDomainWith(positive_ref, Domain(bound))) {
827  return false;
828  }
829  continue;
830  }
831  if (implied.IsFixed()) {
832  // Corner case.
833  if (implied.FixedValue() == bound) {
834  context->UpdateRuleStats("dual: fix variable");
835  if (!context->IntersectDomainWith(positive_ref, implied)) {
836  return false;
837  }
838  continue;
839  }
840 
841  // Note(user): If we have enforced => var fixed, we could actually
842  // just have removed var from the constraint it it was implied by
843  // another constraint. If not, because of the new affine relation we
844  // could remove it right away.
845  processed[PositiveRef(enf)] = true;
846  processed[positive_ref] = true;
847  context->UpdateRuleStats("dual: affine relation");
848  if (RefIsPositive(enf)) {
849  // positive_ref = enf * implied + (1 - enf) * bound.
850  if (!context->StoreAffineRelation(
851  positive_ref, enf, implied.FixedValue() - bound, bound)) {
852  return false;
853  }
854  } else {
855  // positive_ref = (1 - enf) * implied + enf * bound.
856  if (!context->StoreAffineRelation(positive_ref, PositiveRef(enf),
857  bound - implied.FixedValue(),
858  implied.FixedValue())) {
859  return false;
860  }
861  }
862  continue;
863  }
864 
865  if (context->CanBeUsedAsLiteral(positive_ref)) {
866  // If we have a literal, we always add the implication.
867  // This seems like a good thing to do.
868  processed[PositiveRef(enf)] = true;
869  processed[positive_ref] = true;
870  context->UpdateRuleStats("dual: add implication");
871  context->AddImplication(NegatedRef(enf), NegatedRef(ref));
872  context->UpdateNewConstraintsVariableUsage();
873  continue;
874  }
875 
876  // We can add an implication not_enforced => var to its bound ?
877  context->UpdateRuleStats("TODO dual: add implied bound");
878  }
879 
880  // We can make enf equivalent to the constraint instead of just =>. This
881  // seems useful since internally we always use fully reified encoding.
882  if (ct.constraint_case() == ConstraintProto::kLinear &&
883  ct.linear().vars().size() == 1 &&
884  ct.enforcement_literal().size() == 1 &&
885  ct.enforcement_literal(0) == NegatedRef(ref)) {
886  const int var = ct.linear().vars(0);
887  const Domain var_domain = context->DomainOf(var);
888  const Domain rhs = ReadDomainFromProto(ct.linear())
889  .InverseMultiplicationBy(ct.linear().coeffs(0))
890  .IntersectionWith(var_domain);
891  if (rhs.IsEmpty()) {
892  context->UpdateRuleStats("linear1: infeasible");
893  if (!context->SetLiteralToFalse(ct.enforcement_literal(0))) {
894  return false;
895  }
896  processed[PositiveRef(ref)] = true;
897  processed[PositiveRef(var)] = true;
898  context->working_model->mutable_constraints(ct_index)->Clear();
899  context->UpdateConstraintVariableUsage(ct_index);
900  continue;
901  }
902  if (rhs == var_domain) {
903  context->UpdateRuleStats("linear1: always true");
904  processed[PositiveRef(ref)] = true;
905  processed[PositiveRef(var)] = true;
906  context->working_model->mutable_constraints(ct_index)->Clear();
907  context->UpdateConstraintVariableUsage(ct_index);
908  continue;
909  }
910 
911  const Domain complement = rhs.Complement().IntersectionWith(var_domain);
912  if (rhs.IsFixed() || complement.IsFixed()) {
913  context->UpdateRuleStats("dual: make encoding equiv");
914  const int64_t value =
915  rhs.IsFixed() ? rhs.FixedValue() : complement.FixedValue();
916  int encoding_lit;
917  if (context->HasVarValueEncoding(var, value, &encoding_lit)) {
918  // If it is different, we have an equivalence now, and we can
919  // remove the constraint.
920  if (rhs.IsFixed()) {
921  if (encoding_lit == NegatedRef(ref)) continue;
922  context->StoreBooleanEqualityRelation(encoding_lit,
923  NegatedRef(ref));
924  } else {
925  if (encoding_lit == ref) continue;
926  context->StoreBooleanEqualityRelation(encoding_lit, ref);
927  }
928  context->working_model->mutable_constraints(ct_index)->Clear();
929  context->UpdateConstraintVariableUsage(ct_index);
930  processed[PositiveRef(ref)] = true;
931  processed[PositiveRef(var)] = true;
932  processed[PositiveRef(encoding_lit)] = true;
933  continue;
934  }
935 
936  processed[PositiveRef(ref)] = true;
937  processed[PositiveRef(var)] = true;
938  ConstraintProto* new_ct = context->working_model->add_constraints();
939  new_ct->add_enforcement_literal(ref);
940  new_ct->mutable_linear()->add_vars(var);
941  new_ct->mutable_linear()->add_coeffs(1);
942  FillDomainInProto(complement, new_ct->mutable_linear());
943  context->UpdateNewConstraintsVariableUsage();
944 
945  if (rhs.IsFixed()) {
946  context->StoreLiteralImpliesVarEqValue(NegatedRef(ref), var, value);
947  context->StoreLiteralImpliesVarNEqValue(ref, var, value);
948  } else if (complement.IsFixed()) {
949  context->StoreLiteralImpliesVarNEqValue(NegatedRef(ref), var,
950  value);
951  context->StoreLiteralImpliesVarEqValue(ref, var, value);
952  }
953  continue;
954  }
955  }
956 
957  // If We have two Booleans with a blocking constraint that differ just
958  // on them, we can make the Boolean equivalent. This is because they
959  // will be forced to their bad value only if it is needed for that
960  // constraint.
961  //
962  // TODO(user): Generalize to non-Boolean. Also for Boolean, we might
963  // miss some possible reduction if replacing X by 1 - X make a constraint
964  // near-duplicate of another.
965  //
966  // TODO(user): We can generalize to non-linear constraint.
967  //
968  // TODO(user): Because this can be in num_var ^ 2 in some bad cases where
969  // each variable is only blocked by a long constraint, we impose a work
970  // limit. Improve?
971  if (ct.constraint_case() == ConstraintProto::kLinear &&
972  context->CanBeUsedAsLiteral(ref) && work_done < work_limit) {
973  work_done += ct.linear().vars().size();
974  TransformLinearWithSpecialBoolean(ct, ref, &temp_data);
975  const uint64_t hash =
976  fasthash64(temp_data.data(), temp_data.size() * sizeof(int64_t),
977  uint64_t{0xa5b85c5e198ed849});
978  const auto [it, inserted] =
979  equiv_modified_constraints.insert({hash, {ct_index, ref}});
980  if (!inserted) {
981  // Already present!
982  const auto [other_c_with_same_hash, other_ref] = it->second;
983  CHECK_NE(other_c_with_same_hash, ct_index);
984  const auto& other_ct =
985  context->working_model->constraints(other_c_with_same_hash);
986  TransformLinearWithSpecialBoolean(other_ct, other_ref,
987  &other_temp_data);
988  if (temp_data == other_temp_data) {
989  // We have a true equality. The two ref can be made equivalent.
990  if (!processed[PositiveRef(other_ref)]) {
991  ++num_bool_in_near_duplicate_ct;
992  processed[PositiveRef(ref)] = true;
993  processed[PositiveRef(other_ref)] = true;
994  context->StoreBooleanEqualityRelation(ref, other_ref);
995 
996  // We can delete one of the constraint since they are duplicate
997  // now.
998  ++num_deleted_constraints_;
999  context->working_model->mutable_constraints(ct_index)->Clear();
1000  context->UpdateConstraintVariableUsage(ct_index);
1001  continue;
1002  }
1003  }
1004  }
1005  }
1006 
1007  // Other potential cases?
1008  if (!ct.enforcement_literal().empty()) {
1009  if (ct.constraint_case() == ConstraintProto::kLinear &&
1010  ct.linear().vars().size() == 1 &&
1011  ct.enforcement_literal().size() == 1 &&
1012  ct.enforcement_literal(0) == NegatedRef(ref)) {
1013  context->UpdateRuleStats("TODO dual: make linear1 equiv");
1014  } else {
1015  context->UpdateRuleStats(
1016  "TODO dual: only one blocking enforced constraint?");
1017  }
1018  } else {
1019  context->UpdateRuleStats("TODO dual: only one blocking constraint?");
1020  }
1021  continue;
1022  }
1023  if (ct.enforcement_literal().size() != 1) continue;
1024 
1025  // If (a => b) is the only constraint blocking a literal a in the up
1026  // direction, then we can set a == b !
1027  //
1028  // Recover a => b where a is having an unique up_lock (i.e this constraint).
1029  // Note that if many implications are encoded in the same bool_and, we have
1030  // to be careful that a is appearing in just one of them.
1031  //
1032  // TODO(user): Make sure implication graph is transitively reduced to not
1033  // miss such reduction. More generally, this might only use the graph rather
1034  // than the encoding into bool_and / at_most_one ? Basically if a =>
1035  // all_direct_deduction, we can transform it into a <=> all_direct_deduction
1036  // if that is interesting. This could always be done on a max-2sat problem
1037  // in one of the two direction. Also think about max-2sat specific presolve.
1038  int a = ct.enforcement_literal(0);
1039  int b = 1;
1040  if (PositiveRef(a) == positive_ref &&
1041  num_locks_[RefToIntegerVariable(NegatedRef(a))] == 1) {
1042  // Here, we can only add the equivalence if the literal is the only
1043  // on the lhs, otherwise there is actually more lock.
1044  if (ct.bool_and().literals().size() != 1) continue;
1045  b = ct.bool_and().literals(0);
1046  } else {
1047  bool found = false;
1048  b = NegatedRef(ct.enforcement_literal(0));
1049  for (const int lhs : ct.bool_and().literals()) {
1050  if (PositiveRef(lhs) == positive_ref &&
1051  num_locks_[RefToIntegerVariable(lhs)] == 1) {
1052  found = true;
1053  a = NegatedRef(lhs);
1054  break;
1055  }
1056  }
1057  CHECK(found);
1058  }
1059  CHECK_EQ(num_locks_[RefToIntegerVariable(NegatedRef(a))], 1);
1060 
1061  processed[PositiveRef(a)] = true;
1062  processed[PositiveRef(b)] = true;
1063  context->StoreBooleanEqualityRelation(a, b);
1064  context->UpdateRuleStats("dual: enforced equivalence");
1065  }
1066 
1067  if (num_bool_in_near_duplicate_ct) {
1068  context->UpdateRuleStats(
1069  "dual: equivalent Boolean in near-duplicate constraints",
1070  num_bool_in_near_duplicate_ct);
1071  }
1072 
1073  VLOG(2) << "Num deleted constraints: " << num_deleted_constraints_;
1074  return true;
1075 }
1076 
1078  const PresolveContext& context, VarDomination* var_domination,
1079  DualBoundStrengthening* dual_bound_strengthening) {
1080  const CpModelProto& cp_model = *context.working_model;
1081  const int num_vars = cp_model.variables().size();
1082  var_domination->Reset(num_vars);
1083  dual_bound_strengthening->Reset(num_vars);
1084 
1085  for (int var = 0; var < num_vars; ++var) {
1086  // Ignore variables that have been substitued already or are unused.
1087  if (context.IsFixed(var) || context.VariableWasRemoved(var) ||
1088  context.VariableIsNotUsedAnymore(var)) {
1089  dual_bound_strengthening->CannotMove({var});
1090  var_domination->CanOnlyDominateEachOther({var});
1091  continue;
1092  }
1093 
1094  // Deal with the affine relations that are not part of the proto.
1095  // Those only need to be processed in the first pass.
1096  const AffineRelation::Relation r = context.GetAffineRelation(var);
1097  if (r.representative != var) {
1098  dual_bound_strengthening->CannotMove({var, r.representative});
1099  if (r.coeff == 1) {
1100  var_domination->CanOnlyDominateEachOther(
1101  {NegatedRef(var), r.representative});
1102  } else if (r.coeff == -1) {
1103  var_domination->CanOnlyDominateEachOther({var, r.representative});
1104  } else {
1105  var_domination->CanOnlyDominateEachOther({var});
1106  var_domination->CanOnlyDominateEachOther({r.representative});
1107  }
1108  }
1109  }
1110 
1111  // TODO(user): Benchmark and experiment with 3 phases algo:
1112  // - Only ActivityShouldNotChange()/CanOnlyDominateEachOther().
1113  // - The other cases once.
1114  // - EndFirstPhase() and then the other cases a second time.
1115  std::vector<int> tmp;
1116  const int num_constraints = cp_model.constraints_size();
1117  for (int phase = 0; phase < 2; phase++) {
1118  for (int c = 0; c < num_constraints; ++c) {
1119  const ConstraintProto& ct = cp_model.constraints(c);
1120  if (phase == 0) {
1121  dual_bound_strengthening->CannotIncrease(ct.enforcement_literal(), c);
1122  }
1123  switch (ct.constraint_case()) {
1124  case ConstraintProto::kBoolOr:
1125  if (phase == 0) {
1126  dual_bound_strengthening->CannotDecrease(ct.bool_or().literals(),
1127  c);
1128  }
1129  var_domination->ActivityShouldNotDecrease(ct.enforcement_literal(),
1130  ct.bool_or().literals(),
1131  /*coeffs=*/{});
1132  break;
1133  case ConstraintProto::kBoolAnd:
1134  if (phase == 0) {
1135  dual_bound_strengthening->CannotDecrease(ct.bool_and().literals(),
1136  c);
1137  }
1138 
1139  // We process it like n clauses.
1140  //
1141  // TODO(user): the way we process that is a bit restrictive. By
1142  // working on the implication graph we could detect more dominance
1143  // relations. Since if a => b we say that a++ can only be paired with
1144  // b--, but it could actually be paired with any variables that when
1145  // dereased implies b = 0. This is a bit mitigated by the fact that
1146  // we regroup when we can such implications into big at most ones.
1147  tmp.clear();
1148  for (const int ref : ct.enforcement_literal()) {
1149  tmp.push_back(NegatedRef(ref));
1150  }
1151  for (const int ref : ct.bool_and().literals()) {
1152  tmp.push_back(ref);
1153  var_domination->ActivityShouldNotDecrease(/*enforcements=*/{}, tmp,
1154  /*coeffs=*/{});
1155  tmp.pop_back();
1156  }
1157  break;
1158  case ConstraintProto::kAtMostOne:
1159  if (phase == 0) {
1160  dual_bound_strengthening->CannotIncrease(
1161  ct.at_most_one().literals(), c);
1162  }
1163  var_domination->ActivityShouldNotIncrease(ct.enforcement_literal(),
1164  ct.at_most_one().literals(),
1165  /*coeffs=*/{});
1166  break;
1167  case ConstraintProto::kExactlyOne:
1168  if (phase == 0) {
1169  dual_bound_strengthening->CannotMove(ct.exactly_one().literals(),
1170  c);
1171  }
1172  var_domination->ActivityShouldNotChange(ct.exactly_one().literals(),
1173  /*coeffs=*/{});
1174  break;
1175  case ConstraintProto::kLinear: {
1176  // TODO(user): Maybe we should avoid recomputing that here.
1177  const auto [min_activity, max_activity] =
1178  context.ComputeMinMaxActivity(ct.linear());
1179  if (phase == 0) {
1180  dual_bound_strengthening->ProcessLinearConstraint(
1181  false, context, ct.linear(), min_activity, max_activity, c);
1182  }
1183  const bool domain_is_simple = ct.linear().domain().size() == 2;
1184  const bool free_to_increase =
1185  domain_is_simple && ct.linear().domain(1) >= max_activity;
1186  const bool free_to_decrease =
1187  domain_is_simple && ct.linear().domain(0) <= min_activity;
1188  if (free_to_decrease && free_to_increase) break;
1189  if (free_to_increase) {
1190  var_domination->ActivityShouldNotDecrease(ct.enforcement_literal(),
1191  ct.linear().vars(),
1192  ct.linear().coeffs());
1193  } else if (free_to_decrease) {
1194  var_domination->ActivityShouldNotIncrease(ct.enforcement_literal(),
1195  ct.linear().vars(),
1196  ct.linear().coeffs());
1197  } else {
1198  // TODO(user): Handle enforcement better here.
1199  if (!ct.enforcement_literal().empty()) {
1200  var_domination->ActivityShouldNotIncrease(
1201  /*enforcements=*/{}, ct.enforcement_literal(), /*coeffs=*/{});
1202  }
1203  var_domination->ActivityShouldNotChange(ct.linear().vars(),
1204  ct.linear().coeffs());
1205  }
1206  break;
1207  }
1208  default:
1209  // We cannot infer anything if we don't know the constraint.
1210  // TODO(user): Handle enforcement better here.
1211  if (phase == 0) {
1212  dual_bound_strengthening->CannotMove(context.ConstraintToVars(c),
1213  c);
1214  }
1215  for (const int var : context.ConstraintToVars(c)) {
1216  var_domination->CanOnlyDominateEachOther({var});
1217  }
1218  break;
1219  }
1220  }
1221 
1222  // The objective is handled like a <= constraints, or an == constraint if
1223  // there is a non-trivial domain.
1224  if (cp_model.has_objective()) {
1225  // WARNING: The proto objective might not be up to date, so we need to
1226  // write it first.
1227  if (phase == 0) {
1228  context.WriteObjectiveToProto();
1229  }
1230  const auto [min_activity, max_activity] =
1231  context.ComputeMinMaxActivity(cp_model.objective());
1232  const auto& domain = cp_model.objective().domain();
1233  if (phase == 0 && !domain.empty()) {
1234  dual_bound_strengthening->ProcessLinearConstraint(
1235  true, context, cp_model.objective(), min_activity, max_activity);
1236  }
1237  if (domain.empty() || (domain.size() == 2 && domain[0] <= min_activity)) {
1238  var_domination->ActivityShouldNotIncrease(
1239  /*enforcements=*/{}, cp_model.objective().vars(),
1240  cp_model.objective().coeffs());
1241  } else {
1242  var_domination->ActivityShouldNotChange(cp_model.objective().vars(),
1243  cp_model.objective().coeffs());
1244  }
1245  }
1246 
1247  if (phase == 0) {
1248  // Early abort if no possible relations can be found.
1249  //
1250  // TODO(user): We might be able to detect that nothing can be done earlier
1251  // during the constraint scanning.
1252  if (!var_domination->EndFirstPhase()) return;
1253  }
1254  if (phase == 1) var_domination->EndSecondPhase();
1255  }
1256 
1257  // Some statistics.
1258  int64_t num_unconstrained_refs = 0;
1259  int64_t num_dominated_refs = 0;
1260  int64_t num_dominance_relations = 0;
1261  for (int var = 0; var < num_vars; ++var) {
1262  if (context.IsFixed(var)) continue;
1263 
1264  for (const int ref : {var, NegatedRef(var)}) {
1265  if (var_domination->CanFreelyDecrease(ref)) {
1266  num_unconstrained_refs++;
1267  } else if (!var_domination->DominatingVariables(ref).empty()) {
1268  num_dominated_refs++;
1269  num_dominance_relations +=
1270  var_domination->DominatingVariables(ref).size();
1271  }
1272  }
1273  }
1274  if (num_unconstrained_refs == 0 && num_dominated_refs == 0) return;
1275  VLOG(1) << "Dominance:"
1276  << " num_unconstrained_refs=" << num_unconstrained_refs
1277  << " num_dominated_refs=" << num_dominated_refs
1278  << " num_dominance_relations=" << num_dominance_relations;
1279 }
1280 
1281 namespace {
1282 
1283 bool ProcessAtMostOne(absl::Span<const int> literals,
1284  const std::string& message,
1285  const VarDomination& var_domination,
1287  PresolveContext* context) {
1288  for (const int ref : literals) {
1289  (*in_constraints)[VarDomination::RefToIntegerVariable(ref)] = true;
1290  }
1291  for (const int ref : literals) {
1292  if (context->IsFixed(ref)) continue;
1293 
1294  const auto dominating_ivars = var_domination.DominatingVariables(ref);
1295  if (dominating_ivars.empty()) continue;
1296  for (const IntegerVariable ivar : dominating_ivars) {
1297  if (!(*in_constraints)[ivar]) continue;
1298  if (context->IsFixed(VarDomination::IntegerVariableToRef(ivar))) {
1299  continue;
1300  }
1301 
1302  // We can set the dominated variable to false.
1303  context->UpdateRuleStats(message);
1304  if (!context->SetLiteralToFalse(ref)) return false;
1305  break;
1306  }
1307  }
1308  for (const int ref : literals) {
1309  (*in_constraints)[VarDomination::RefToIntegerVariable(ref)] = false;
1310  }
1311  return true;
1312 }
1313 
1314 } // namespace
1315 
1316 bool ExploitDominanceRelations(const VarDomination& var_domination,
1318  const CpModelProto& cp_model = *context->working_model;
1319  const int num_vars = cp_model.variables_size();
1320 
1321  // Abort early if there is nothing to do.
1322  bool work_to_do = false;
1323  for (int var = 0; var < num_vars; ++var) {
1324  if (context->IsFixed(var)) continue;
1325  if (!var_domination.DominatingVariables(var).empty() ||
1326  !var_domination.DominatingVariables(NegatedRef(var)).empty()) {
1327  work_to_do = true;
1328  break;
1329  }
1330  }
1331  if (!work_to_do) return true;
1332 
1333  absl::StrongVector<IntegerVariable, int64_t> var_lb_to_ub_diff(num_vars * 2,
1334  0);
1335  absl::StrongVector<IntegerVariable, bool> in_constraints(num_vars * 2, false);
1336 
1337  absl::flat_hash_set<std::pair<int, int>> implications;
1338  const int num_constraints = cp_model.constraints_size();
1339  for (int c = 0; c < num_constraints; ++c) {
1340  const ConstraintProto& ct = cp_model.constraints(c);
1341 
1342  if (ct.constraint_case() == ConstraintProto::kBoolAnd) {
1343  if (ct.enforcement_literal().size() != 1) continue;
1344  const int a = ct.enforcement_literal(0);
1345  if (context->IsFixed(a)) continue;
1346  for (const int b : ct.bool_and().literals()) {
1347  if (context->IsFixed(b)) continue;
1348  implications.insert({a, b});
1349  implications.insert({NegatedRef(b), NegatedRef(a)});
1350 
1351  // If (a--, b--) is valid, we can always set a to false.
1352  for (const IntegerVariable ivar :
1353  var_domination.DominatingVariables(a)) {
1354  const int ref = VarDomination::IntegerVariableToRef(ivar);
1355  if (ref == NegatedRef(b)) {
1356  context->UpdateRuleStats("domination: in implication");
1357  if (!context->SetLiteralToFalse(a)) return false;
1358  break;
1359  }
1360  }
1361  if (context->IsFixed(a)) break;
1362 
1363  // If (b++, a++) is valid, then we can always set b to true.
1364  for (const IntegerVariable ivar :
1365  var_domination.DominatingVariables(NegatedRef(b))) {
1366  const int ref = VarDomination::IntegerVariableToRef(ivar);
1367  if (ref == a) {
1368  context->UpdateRuleStats("domination: in implication");
1369  if (!context->SetLiteralToTrue(b)) return false;
1370  break;
1371  }
1372  }
1373  }
1374  continue;
1375  }
1376 
1377  if (!ct.enforcement_literal().empty()) continue;
1378 
1379  // TODO(user): More generally, combine with probing? if a dominated variable
1380  // implies one of its dominant to zero, then it can be set to zero. It seems
1381  // adding the implication below should have the same effect? but currently
1382  // it requires a lot of presolve rounds.
1383  if (ct.constraint_case() == ConstraintProto::kAtMostOne) {
1384  if (!ProcessAtMostOne(ct.at_most_one().literals(),
1385  "domination: in at most one", var_domination,
1386  &in_constraints, context)) {
1387  return false;
1388  }
1389  } else if (ct.constraint_case() == ConstraintProto::kExactlyOne) {
1390  if (!ProcessAtMostOne(ct.exactly_one().literals(),
1391  "domination: in exactly one", var_domination,
1392  &in_constraints, context)) {
1393  return false;
1394  }
1395  }
1396 
1397  if (ct.constraint_case() != ConstraintProto::kLinear) continue;
1398 
1399  int num_dominated = 0;
1400  for (const int var : context->ConstraintToVars(c)) {
1401  if (!var_domination.DominatingVariables(var).empty()) ++num_dominated;
1402  if (!var_domination.DominatingVariables(NegatedRef(var)).empty()) {
1403  ++num_dominated;
1404  }
1405  }
1406  if (num_dominated == 0) continue;
1407 
1408  // Precompute.
1409  int64_t min_activity = 0;
1410  int64_t max_activity = 0;
1411  const int num_terms = ct.linear().vars_size();
1412  for (int i = 0; i < num_terms; ++i) {
1413  int ref = ct.linear().vars(i);
1414  int64_t coeff = ct.linear().coeffs(i);
1415  if (coeff < 0) {
1416  ref = NegatedRef(ref);
1417  coeff = -coeff;
1418  }
1419  const int64_t min_term = coeff * context->MinOf(ref);
1420  const int64_t max_term = coeff * context->MaxOf(ref);
1421  min_activity += min_term;
1422  max_activity += max_term;
1423  const IntegerVariable ivar = VarDomination::RefToIntegerVariable(ref);
1424  var_lb_to_ub_diff[ivar] = max_term - min_term;
1425  var_lb_to_ub_diff[NegationOf(ivar)] = min_term - max_term;
1426  }
1427  const int64_t rhs_lb = ct.linear().domain(0);
1428  const int64_t rhs_ub = ct.linear().domain(ct.linear().domain_size() - 1);
1429  if (max_activity < rhs_lb || min_activity > rhs_ub) {
1430  return context->NotifyThatModelIsUnsat("linear equation unsat.");
1431  }
1432 
1433  // Look for dominated var.
1434  for (int i = 0; i < num_terms; ++i) {
1435  const int ref = ct.linear().vars(i);
1436  const int64_t coeff = ct.linear().coeffs(i);
1437  const int64_t coeff_magnitude = std::abs(coeff);
1438  if (context->IsFixed(ref)) continue;
1439 
1440  for (const int current_ref : {ref, NegatedRef(ref)}) {
1441  const absl::Span<const IntegerVariable> dominated_by =
1442  var_domination.DominatingVariables(current_ref);
1443  if (dominated_by.empty()) continue;
1444 
1445  const bool ub_side = (coeff > 0) == (current_ref == ref);
1446  if (ub_side) {
1447  if (max_activity <= rhs_ub) continue;
1448  } else {
1449  if (min_activity >= rhs_lb) continue;
1450  }
1451  const int64_t slack =
1452  ub_side ? rhs_ub - min_activity : max_activity - rhs_lb;
1453 
1454  // Compute the delta in activity if all dominating var moves to their
1455  // other bound.
1456  int64_t delta = 0;
1457  for (const IntegerVariable ivar : dominated_by) {
1458  // Tricky: For now we skip complex domain as we are not sure they
1459  // can be moved correctly.
1460  if (context->DomainOf(VarDomination::IntegerVariableToRef(ivar))
1461  .NumIntervals() != 1) {
1462  continue;
1463  }
1464  if (ub_side) {
1465  delta += std::max(int64_t{0}, var_lb_to_ub_diff[ivar]);
1466  } else {
1467  delta += std::max(int64_t{0}, -var_lb_to_ub_diff[ivar]);
1468  }
1469  }
1470 
1471  const int64_t lb = context->MinOf(current_ref);
1472  if (delta + coeff_magnitude > slack) {
1473  context->UpdateRuleStats("domination: fixed to lb.");
1474  if (!context->IntersectDomainWith(current_ref, Domain(lb))) {
1475  return false;
1476  }
1477 
1478  // We need to update the precomputed quantities.
1479  const IntegerVariable current_var =
1481  if (ub_side) {
1482  CHECK_GE(var_lb_to_ub_diff[current_var], 0);
1483  max_activity -= var_lb_to_ub_diff[current_var];
1484  } else {
1485  CHECK_LE(var_lb_to_ub_diff[current_var], 0);
1486  min_activity -= var_lb_to_ub_diff[current_var];
1487  }
1488  var_lb_to_ub_diff[current_var] = 0;
1489  var_lb_to_ub_diff[NegationOf(current_var)] = 0;
1490 
1491  continue;
1492  }
1493 
1494  const IntegerValue diff = FloorRatio(IntegerValue(slack - delta),
1495  IntegerValue(coeff_magnitude));
1496  int64_t new_ub = lb + diff.value();
1497  if (new_ub < context->MaxOf(current_ref)) {
1498  // Tricky: If there are holes, we can't just reduce the domain to
1499  // new_ub if it is not a valid value, so we need to compute the Min()
1500  // of the intersection.
1501  new_ub = context->DomainOf(current_ref)
1502  .IntersectionWith(
1504  .Min();
1505  }
1506  if (new_ub < context->MaxOf(current_ref)) {
1507  context->UpdateRuleStats("domination: reduced ub.");
1508  if (!context->IntersectDomainWith(current_ref, Domain(lb, new_ub))) {
1509  return false;
1510  }
1511 
1512  // We need to update the precomputed quantities.
1513  const IntegerVariable current_var =
1515  if (ub_side) {
1516  CHECK_GE(var_lb_to_ub_diff[current_var], 0);
1517  max_activity -= var_lb_to_ub_diff[current_var];
1518  } else {
1519  CHECK_LE(var_lb_to_ub_diff[current_var], 0);
1520  min_activity -= var_lb_to_ub_diff[current_var];
1521  }
1522  const int64_t new_diff = std::abs(coeff_magnitude * (new_ub - lb));
1523  if (ub_side) {
1524  var_lb_to_ub_diff[current_var] = new_diff;
1525  var_lb_to_ub_diff[NegationOf(current_var)] = -new_diff;
1526  max_activity += new_diff;
1527  } else {
1528  var_lb_to_ub_diff[current_var] = -new_diff;
1529  var_lb_to_ub_diff[NegationOf(current_var)] = +new_diff;
1530  min_activity -= new_diff;
1531  }
1532  }
1533  }
1534  }
1535 
1536  // Restore.
1537  for (const int ref : ct.linear().vars()) {
1538  const IntegerVariable ivar = VarDomination::RefToIntegerVariable(ref);
1539  var_lb_to_ub_diff[ivar] = 0;
1540  var_lb_to_ub_diff[NegationOf(ivar)] = 0;
1541  }
1542  }
1543 
1544  // For any dominance relation still left (i.e. between non-fixed vars), if
1545  // the variable are Boolean and X is dominated by Y, we can add
1546  // (X = 1) => (Y = 1). But, as soon as we do that, we break some symmetry
1547  // and cannot add any incompatible relations.
1548  //
1549  // EX: It is possible that X dominate Y and Y dominate X if they are both
1550  // appearing in exactly the same constraint with the same coefficient.
1551  //
1552  // TODO(user): if both variable are in a bool_or, this will allow us to remove
1553  // the dominated variable. Maybe we should exploit that to decide which
1554  // implication we add. Or just remove such variable and not add the
1555  // implications?
1556  //
1557  // TODO(user): generalize to non Booleans?
1558  // TODO(user): We always keep adding the same relations. Investigate?
1559  // it seems pure SAT presolve remove them.
1560  int num_added = 0;
1561  absl::StrongVector<IntegerVariable, bool> increase_is_forbidden(2 * num_vars,
1562  false);
1563  for (int positive_ref = 0; positive_ref < num_vars; ++positive_ref) {
1564  if (context->IsFixed(positive_ref)) continue;
1565  if (context->VariableIsNotUsedAnymore(positive_ref)) continue;
1566  if (context->VariableWasRemoved(positive_ref)) continue;
1567  if (!context->CanBeUsedAsLiteral(positive_ref)) continue;
1568  for (const int ref : {positive_ref, NegatedRef(positive_ref)}) {
1569  const IntegerVariable var = VarDomination::RefToIntegerVariable(ref);
1570  if (increase_is_forbidden[NegationOf(var)]) continue;
1571  for (const IntegerVariable dom :
1572  var_domination.DominatingVariables(ref)) {
1573  if (increase_is_forbidden[dom]) continue;
1574  const int dom_ref = VarDomination::IntegerVariableToRef(dom);
1575  if (context->IsFixed(dom_ref)) continue;
1576  if (context->VariableIsNotUsedAnymore(dom_ref)) continue;
1577  if (context->VariableWasRemoved(dom_ref)) continue;
1578  if (!context->CanBeUsedAsLiteral(dom_ref)) continue;
1579  if (implications.contains({ref, dom_ref})) continue;
1580 
1581  ++num_added;
1582  context->AddImplication(ref, dom_ref);
1583 
1584  // dom-- or var++ are now forbidden.
1585  increase_is_forbidden[var] = true;
1586  increase_is_forbidden[NegationOf(dom)] = true;
1587  }
1588  }
1589  }
1590  if (num_added > 0) {
1591  VLOG(1) << "Added " << num_added << " domination implications.";
1592  context->UpdateNewConstraintsVariableUsage();
1593  context->UpdateRuleStats("domination: added implications", num_added);
1594  }
1595 
1596  return true;
1597 }
1598 
1599 } // namespace sat
1600 } // namespace operations_research
int64_t max
Definition: alldiff_cst.cc:140
int64_t min
Definition: alldiff_cst.cc:139
void assign(size_type n, const value_type &val)
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].
Domain InverseMultiplicationBy(const int64_t coeff) const
Returns {x ∈ Int64, ∃ e ∈ D, x * coeff = e}.
Domain Complement() const
Returns the set Int64 ∖ D.
bool Contains(int64_t value) const
Returns true iff value is in Domain.
int64_t FixedValue() const
Returns the value of a fixed domain.
std::vector< int64_t > FlattenedIntervals() const
This method returns the flattened list of interval bounds of the domain.
bool IsFixed() const
Returns true iff the domain is reduced to a single value.
Domain IntersectionWith(const Domain &domain) const
Returns the intersection of D and domain.
int64_t Min() const
Returns the min value of the domain.
bool IsEmpty() const
Returns true if this is the empty set.
void ProcessLinearConstraint(bool is_objective, const PresolveContext &context, const LinearProto &linear, int64_t min_activity, int64_t max_activity, int ct_index=-1)
void CannotMove(absl::Span< const int > refs, int ct_index=-1)
void CannotIncrease(absl::Span< const int > refs, int ct_index=-1)
void CannotDecrease(absl::Span< const int > refs, int ct_index=-1)
void ActivityShouldNotIncrease(absl::Span< const int > enforcements, absl::Span< const int > refs, absl::Span< const int64_t > coeffs)
void ActivityShouldNotChange(absl::Span< const int > refs, absl::Span< const int64_t > coeffs)
static int IntegerVariableToRef(IntegerVariable var)
void ActivityShouldNotDecrease(absl::Span< const int > enforcements, absl::Span< const int > refs, absl::Span< const int64_t > coeffs)
absl::Span< const IntegerVariable > DominatingVariables(int ref) const
std::string DominationDebugString(IntegerVariable var) const
static IntegerVariable RefToIntegerVariable(int ref)
void CanOnlyDominateEachOther(absl::Span< const int > refs)
int64_t b
int64_t a
DecisionBuilder *const phase
const Constraint * ct
int64_t value
IntVar * var
Definition: expr_array.cc:1874
GurobiMPCallbackContext * context
int64_t hash
Definition: matrix_utils.cc:63
void STLClearObject(T *obj)
Definition: stl_util.h:123
IntegerValue FloorRatio(IntegerValue dividend, IntegerValue positive_divisor)
Definition: integer.h:98
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
bool RefIsPositive(int ref)
IntegerValue CeilRatio(IntegerValue dividend, IntegerValue positive_divisor)
Definition: integer.h:89
const IntegerVariable kNoIntegerVariable(-1)
void DetectDominanceRelations(const PresolveContext &context, VarDomination *var_domination, DualBoundStrengthening *dual_bound_strengthening)
IntegerVariable PositiveVariable(IntegerVariable i)
Definition: integer.h:149
void FillDomainInProto(const Domain &domain, ProtoWithDomain *proto)
std::vector< IntegerVariable > NegationOf(const std::vector< IntegerVariable > &vars)
Definition: integer.cc:46
Domain ReadDomainFromProto(const ProtoWithDomain &proto)
bool ExploitDominanceRelations(const VarDomination &var_domination, PresolveContext *context)
Collection of objects used to extend the Constraint Solver library.
uint64_t fasthash64(const void *buf, size_t len, uint64_t seed)
Definition: hash.cc:36
Literal literal
Definition: optimization.cc:88
int64_t delta
Definition: resource.cc:1695
Fractional coeff_magnitude
int64_t bound
int64_t start
std::string message
Definition: trace.cc:399
#define VLOG(verboselevel)
Definition: vlog.h:39