Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
var_domination.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
18#include <algorithm>
19#include <cstdint>
20#include <cstdlib>
21#include <limits>
22#include <memory>
23#include <optional>
24#include <string>
25#include <utility>
26#include <vector>
27
28#include "absl/container/btree_set.h"
29#include "absl/container/flat_hash_map.h"
30#include "absl/container/flat_hash_set.h"
31#include "absl/log/check.h"
32#include "absl/meta/type_traits.h"
33#include "absl/strings/str_cat.h"
34#include "absl/strings/string_view.h"
35#include "absl/types/span.h"
37#include "ortools/base/hash.h"
48#include "ortools/sat/util.h"
53
54namespace operations_research {
55namespace sat {
56
57void VarDomination::Reset(int num_variables) {
58 phase_ = 0;
59 num_vars_with_negation_ = 2 * num_variables;
60 partition_ =
61 std::make_unique<SimpleDynamicPartition>(num_vars_with_negation_);
62
63 can_freely_decrease_.assign(num_vars_with_negation_, true);
64
65 shared_buffer_.clear();
66 has_initial_candidates_.assign(num_vars_with_negation_, false);
67 initial_candidates_.assign(num_vars_with_negation_, IntegerVariableSpan());
68
69 buffer_.clear();
70 dominating_vars_.assign(num_vars_with_negation_, IntegerVariableSpan());
71
72 ct_index_for_signature_ = 0;
73 block_down_signatures_.assign(num_vars_with_negation_, 0);
74}
75
76void VarDomination::RefinePartition(std::vector<int>* vars) {
77 if (vars->empty()) return;
78 partition_->Refine(*vars);
79 for (int& var : *vars) {
80 const IntegerVariable wrapped(var);
81 can_freely_decrease_[wrapped] = false;
82 can_freely_decrease_[NegationOf(wrapped)] = false;
83 var = NegationOf(wrapped).value();
84 }
85 partition_->Refine(*vars);
86}
87
88void VarDomination::CanOnlyDominateEachOther(absl::Span<const int> refs) {
89 if (phase_ != 0) return;
90 tmp_vars_.clear();
91 for (const int ref : refs) {
92 tmp_vars_.push_back(RefToIntegerVariable(ref).value());
93 }
94 RefinePartition(&tmp_vars_);
95 tmp_vars_.clear();
96}
97
98void VarDomination::ActivityShouldNotChange(absl::Span<const int> refs,
99 absl::Span<const int64_t> coeffs) {
100 if (phase_ != 0) return;
101 FillTempRanks(/*reverse_references=*/false, /*enforcements=*/{}, refs,
102 coeffs);
103 tmp_vars_.clear();
104 for (int i = 0; i < tmp_ranks_.size(); ++i) {
105 if (i > 0 && tmp_ranks_[i].rank != tmp_ranks_[i - 1].rank) {
106 RefinePartition(&tmp_vars_);
107 tmp_vars_.clear();
108 }
109 tmp_vars_.push_back(tmp_ranks_[i].var.value());
110 }
111 RefinePartition(&tmp_vars_);
112 tmp_vars_.clear();
113}
114
115// This correspond to a lower bounded constraint.
116void VarDomination::ProcessTempRanks() {
117 if (phase_ == 0) {
118 // We actually "split" tmp_ranks_ according to the current partition and
119 // process each resulting list independently for a faster algo.
120 ++ct_index_for_signature_;
121 for (IntegerVariableWithRank& entry : tmp_ranks_) {
122 can_freely_decrease_[entry.var] = false;
123 block_down_signatures_[entry.var] |= uint64_t{1}
124 << (ct_index_for_signature_ % 64);
125 entry.part = partition_->PartOf(entry.var.value());
126 }
127 std::stable_sort(
128 tmp_ranks_.begin(), tmp_ranks_.end(),
129 [](const IntegerVariableWithRank& a, const IntegerVariableWithRank& b) {
130 return a.part < b.part;
131 });
132 int start = 0;
133 for (int i = 1; i < tmp_ranks_.size(); ++i) {
134 if (tmp_ranks_[i].part != tmp_ranks_[start].part) {
135 Initialize({&tmp_ranks_[start], static_cast<size_t>(i - start)});
136 start = i;
137 }
138 }
139 if (start < tmp_ranks_.size()) {
140 Initialize({&tmp_ranks_[start], tmp_ranks_.size() - start});
141 }
142 } else if (phase_ == 1) {
143 FilterUsingTempRanks();
144 } else {
145 // This is only used for debugging, and we shouldn't reach here in prod.
146 CheckUsingTempRanks();
147 }
148}
149
151 absl::Span<const int> enforcements, absl::Span<const int> refs,
152 absl::Span<const int64_t> coeffs) {
153 FillTempRanks(/*reverse_references=*/false, enforcements, refs, coeffs);
154 ProcessTempRanks();
155}
156
158 absl::Span<const int> enforcements, absl::Span<const int> refs,
159 absl::Span<const int64_t> coeffs) {
160 FillTempRanks(/*reverse_references=*/true, enforcements, refs, coeffs);
161 ProcessTempRanks();
162}
163
164void VarDomination::MakeRankEqualToStartOfPart(
165 absl::Span<IntegerVariableWithRank> span) {
166 const int size = span.size();
167 int start = 0;
168 int previous_value = 0;
169 for (int i = 0; i < size; ++i) {
170 const int64_t value = span[i].rank;
171 if (value != previous_value) {
172 previous_value = value;
173 start = i;
174 }
175 span[i].rank = start;
176 }
177}
178
179void VarDomination::Initialize(absl::Span<IntegerVariableWithRank> span) {
180 // The rank can be wrong and need to be recomputed because of how we split
181 // tmp_ranks_ into spans.
182 MakeRankEqualToStartOfPart(span);
183
184 // We made sure all the variable belongs to the same part.
185 if (DEBUG_MODE) {
186 if (span.empty()) return;
187 const int part = partition_->PartOf(span[0].var.value());
188 for (int i = 1; i < span.size(); ++i) {
189 CHECK_EQ(part, partition_->PartOf(span[i].var.value()));
190 }
191 }
192
193 const int future_start = shared_buffer_.size();
194 int first_start = -1;
195 const int size = span.size();
196 for (int i = 0; i < size; ++i) {
197 const IntegerVariableWithRank entry = span[i];
198 const int num_candidates = size - entry.rank;
199
200 // Ignore if we already have a shorter list.
201 if (has_initial_candidates_[entry.var]) {
202 if (num_candidates >= initial_candidates_[entry.var].size) continue;
203 }
204
205 if (first_start == -1) first_start = entry.rank;
206 has_initial_candidates_[entry.var] = true;
207 initial_candidates_[entry.var] = {
208 future_start - first_start + static_cast<int>(entry.rank),
209 num_candidates};
210 }
211
212 // Only store what is necessary.
213 // Note that since we share, we will never store more than the problem size.
214 if (first_start == -1) return;
215 for (int i = first_start; i < size; ++i) {
216 shared_buffer_.push_back(span[i].var);
217 }
218}
219
220// TODO(user): Use more heuristics to not miss as much dominance relation when
221// we crop initial lists.
223 CHECK_EQ(phase_, 0);
224 phase_ = 1;
225
226 // Some initial lists ar too long and will be cropped to this size.
227 // We will handle them slightly differently.
228 //
229 // TODO(user): Tune the initial size, 50 might be a bit large, since our
230 // complexity is borned by this number times the number of entries in the
231 // constraints. Still we should in most situation be a lot lower than that.
232 const int kMaxInitialSize = 50;
233 absl::btree_set<IntegerVariable> cropped_vars;
235 num_vars_with_negation_, false);
236
237 // Fill the initial domination candidates.
238 int non_cropped_size = 0;
239 std::vector<IntegerVariable> partition_data;
240 const std::vector<absl::Span<const IntegerVariable>> elements_by_part =
241 partition_->GetParts(&partition_data);
242 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
243 if (can_freely_decrease_[var]) continue;
244
245 const int part = partition_->PartOf(var.value());
246 const int start = buffer_.size();
247 const uint64_t var_sig = block_down_signatures_[var];
248 const uint64_t not_var_sig = block_down_signatures_[NegationOf(var)];
249 absl::Span<const IntegerVariable> to_scan =
250 has_initial_candidates_[var] ? InitialDominatingCandidates(var)
251 : elements_by_part[part];
252
253 // Two modes, either we scan the full list, or a small subset of it.
254 // Not that low variable indices should appear first, so it is better not
255 // to randomize.
256 int new_size = 0;
257 if (to_scan.size() <= 1'000) {
258 for (const IntegerVariable x : to_scan) {
259 if (var_sig & ~block_down_signatures_[x]) continue; // !included.
260 if (block_down_signatures_[NegationOf(x)] & ~not_var_sig) continue;
261 if (PositiveVariable(x) == PositiveVariable(var)) continue;
262 if (can_freely_decrease_[NegationOf(x)]) continue;
263 ++new_size;
264 buffer_.push_back(x);
265 if (new_size >= kMaxInitialSize) {
266 is_cropped[var] = true;
267 cropped_vars.insert(var);
268 }
269 }
270 } else {
271 is_cropped[var] = true;
272 cropped_vars.insert(var);
273 for (int i = 0; i < 200; ++i) {
274 const IntegerVariable x = to_scan[i];
275 if (var_sig & ~block_down_signatures_[x]) continue; // !included.
276 if (block_down_signatures_[NegationOf(x)] & ~not_var_sig) continue;
277 if (PositiveVariable(x) == PositiveVariable(var)) continue;
278 if (can_freely_decrease_[NegationOf(x)]) continue;
279 ++new_size;
280 buffer_.push_back(x);
281 if (new_size >= kMaxInitialSize) break;
282 }
283 }
284
285 if (!is_cropped[var]) non_cropped_size += new_size;
286 dominating_vars_[var] = {start, new_size};
287 }
288
289 // Heuristic: To try not to remove domination relations corresponding to short
290 // lists during transposition (see EndSecondPhase()), we fill the cropped list
291 // with the transpose of the short list relations. This helps finding more
292 // relation in the presence of cropped lists.
293 //
294 // Compute how many extra space we need for transposed values.
295 // Note that it cannot be more than twice.
296 int total_extra_space = 0;
298 num_vars_with_negation_, 0);
299 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
300 for (const IntegerVariable dom : DominatingVariables(var)) {
301 if (!is_cropped[NegationOf(dom)]) continue;
302 ++total_extra_space;
303 extra_space[NegationOf(dom)]++;
304 }
305 }
306
307 // Copy into a new buffer.
308 int copy_index = 0;
309 other_buffer_.resize(buffer_.size() + total_extra_space);
310 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
311 IntegerVariableSpan& s = dominating_vars_[var];
312 for (int i = 0; i < s.size; ++i) {
313 other_buffer_[copy_index + i] = buffer_[s.start + i];
314 }
315 s.start = copy_index;
316 copy_index += s.size + extra_space[var];
317 extra_space[var] = s.size; // Used below.
318 }
319 DCHECK_EQ(copy_index, other_buffer_.size());
320 std::swap(buffer_, other_buffer_);
321 gtl::STLClearObject(&other_buffer_);
322
323 // Fill the free spaces with transposed values.
324 // But do not use new values !
325 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
326 const int start = dominating_vars_[var].start;
327 const int size = extra_space[var];
328 for (int i = 0; i < size; ++i) {
329 const IntegerVariable dom = buffer_[start + i];
330 if (!is_cropped[NegationOf(dom)]) continue;
331 IntegerVariableSpan& s = dominating_vars_[NegationOf(dom)];
332 buffer_[s.start + s.size++] = NegationOf(var);
333 }
334 }
335
336 // Remove any duplicates.
337 //
338 // TODO(user): Maybe we should do that with all lists in case the
339 // input function are called with duplicates too.
340 for (const IntegerVariable var : cropped_vars) {
341 DCHECK(is_cropped[var]);
342 IntegerVariableSpan& s = dominating_vars_[var];
343 if (s.size == 0) continue;
344 IntegerVariable* pt = &buffer_[s.start];
345 std::sort(pt, pt + s.size);
346 const auto end = std::unique(pt, pt + s.size);
347 s.size = end - pt;
348 }
349
350 // We no longer need the first phase memory.
351 VLOG(1) << "Num initial list that where cropped: "
352 << FormatCounter(cropped_vars.size());
353 VLOG(1) << "Shared buffer size: " << FormatCounter(shared_buffer_.size());
354 VLOG(1) << "Non-cropped buffer size: " << FormatCounter(non_cropped_size);
355 VLOG(1) << "Buffer size: " << FormatCounter(buffer_.size());
356 gtl::STLClearObject(&initial_candidates_);
357 gtl::STLClearObject(&shared_buffer_);
358
359 // A second phase is only needed if there are some potential dominance
360 // relations.
361 return !buffer_.empty();
362}
363
365 CHECK_EQ(phase_, 1);
366 phase_ = 2;
367
368 // Perform intersection with transpose.
369 shared_buffer_.clear();
370 initial_candidates_.assign(num_vars_with_negation_, IntegerVariableSpan());
371
372 // Pass 1: count.
373 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
374 for (const IntegerVariable dom : DominatingVariables(var)) {
375 ++initial_candidates_[NegationOf(dom)].size;
376 }
377 }
378
379 // Pass 2: compute starts.
380 int start = 0;
381 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
382 initial_candidates_[var].start = start;
383 start += initial_candidates_[var].size;
384 initial_candidates_[var].size = 0;
385 }
386 shared_buffer_.resize(start);
387
388 // Pass 3: transpose.
389 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
390 for (const IntegerVariable dom : DominatingVariables(var)) {
391 IntegerVariableSpan& span = initial_candidates_[NegationOf(dom)];
392 shared_buffer_[span.start + span.size++] = NegationOf(var);
393 }
394 }
395
396 // Pass 4: intersect.
397 int num_removed = 0;
398 tmp_var_to_rank_.resize(num_vars_with_negation_, -1);
399 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
400 for (const IntegerVariable dom : InitialDominatingCandidates(var)) {
401 tmp_var_to_rank_[dom] = 1;
402 }
403
404 int new_size = 0;
405 IntegerVariableSpan& span = dominating_vars_[var];
406 for (const IntegerVariable dom : DominatingVariables(var)) {
407 if (tmp_var_to_rank_[dom] != 1) {
408 ++num_removed;
409 continue;
410 }
411 buffer_[span.start + new_size++] = dom;
412 }
413 span.size = new_size;
414
415 for (const IntegerVariable dom : InitialDominatingCandidates(var)) {
416 tmp_var_to_rank_[dom] = -1;
417 }
418 }
419
420 VLOG(1) << "Transpose removed " << FormatCounter(num_removed);
421 gtl::STLClearObject(&initial_candidates_);
422 gtl::STLClearObject(&shared_buffer_);
423}
424
425void VarDomination::FillTempRanks(bool reverse_references,
426 absl::Span<const int> enforcements,
427 absl::Span<const int> refs,
428 absl::Span<const int64_t> coeffs) {
429 tmp_ranks_.clear();
430 if (coeffs.empty()) {
431 // Simple case: all coefficients are assumed to be the same.
432 for (const int ref : refs) {
433 const IntegerVariable var =
434 RefToIntegerVariable(reverse_references ? NegatedRef(ref) : ref);
435 tmp_ranks_.push_back({var, 0, 0});
436 }
437 } else {
438 // Complex case: different coefficients.
439 for (int i = 0; i < refs.size(); ++i) {
440 if (coeffs[i] == 0) continue;
441 const IntegerVariable var = RefToIntegerVariable(
442 reverse_references ? NegatedRef(refs[i]) : refs[i]);
443 if (coeffs[i] > 0) {
444 tmp_ranks_.push_back({var, 0, coeffs[i]});
445 } else {
446 tmp_ranks_.push_back({NegationOf(var), 0, -coeffs[i]});
447 }
448 }
449 std::sort(tmp_ranks_.begin(), tmp_ranks_.end());
450 MakeRankEqualToStartOfPart({&tmp_ranks_[0], tmp_ranks_.size()});
451 }
452
453 // Add the enforcement last with a new rank. We add their negation since
454 // we want the activity to not decrease, and we want to allow any
455 // enforcement-- to dominate a variable in the constraint.
456 const int enforcement_rank = tmp_ranks_.size();
457 for (const int ref : enforcements) {
458 tmp_ranks_.push_back(
459 {RefToIntegerVariable(NegatedRef(ref)), 0, enforcement_rank});
460 }
461}
462
463// We take the intersection of the current dominating candidate with the
464// restriction imposed by the current content of tmp_ranks_.
465void VarDomination::FilterUsingTempRanks() {
466 // Expand ranks in temp vector.
467 tmp_var_to_rank_.resize(num_vars_with_negation_, -1);
468 for (const IntegerVariableWithRank entry : tmp_ranks_) {
469 tmp_var_to_rank_[entry.var] = entry.rank;
470 }
471
472 // The activity of the variable in tmp_rank must not decrease.
473 for (const IntegerVariableWithRank entry : tmp_ranks_) {
474 // The only variables that can be paired with a var-- in the constraints are
475 // the var++ in the constraints with the same rank or higher.
476 //
477 // Note that we only filter the var-- domination lists here, we do not
478 // remove the var-- appearing in all the lists corresponding to wrong var++.
479 // This is left to the transpose operation in EndSecondPhase().
480 {
481 IntegerVariableSpan& span = dominating_vars_[entry.var];
482 if (span.size == 0) continue;
483 int new_size = 0;
484 for (const IntegerVariable candidate : DominatingVariables(entry.var)) {
485 if (tmp_var_to_rank_[candidate] < entry.rank) continue;
486 buffer_[span.start + new_size++] = candidate;
487 }
488 span.size = new_size;
489 }
490 }
491
492 // Reset temporary vector to all -1.
493 for (const IntegerVariableWithRank entry : tmp_ranks_) {
494 tmp_var_to_rank_[entry.var] = -1;
495 }
496}
497
498// Slow: This is for debugging only.
499void VarDomination::CheckUsingTempRanks() {
500 tmp_var_to_rank_.resize(num_vars_with_negation_, -1);
501 for (const IntegerVariableWithRank entry : tmp_ranks_) {
502 tmp_var_to_rank_[entry.var] = entry.rank;
503 }
504
505 // The activity of the variable in tmp_rank must not decrease.
506 for (IntegerVariable var(0); var < num_vars_with_negation_; ++var) {
507 const int var_rank = tmp_var_to_rank_[var];
508 const int negated_var_rank = tmp_var_to_rank_[NegationOf(var)];
509 for (const IntegerVariable dom : DominatingVariables(var)) {
510 CHECK(!can_freely_decrease_[NegationOf(dom)]);
511
512 // Doing X--, Y++ is compatible if the rank[X] <= rank[Y]. But we also
513 // need to check if doing Not(Y)-- is compatible with Not(X)++.
514 CHECK_LE(var_rank, tmp_var_to_rank_[dom]);
515 CHECK_LE(tmp_var_to_rank_[NegationOf(dom)], negated_var_rank);
516 }
517 }
518
519 for (const IntegerVariableWithRank entry : tmp_ranks_) {
520 tmp_var_to_rank_[entry.var] = -1;
521 }
522}
523
527
528bool VarDomination::CanFreelyDecrease(IntegerVariable var) const {
529 return can_freely_decrease_[var];
530}
531
532absl::Span<const IntegerVariable> VarDomination::InitialDominatingCandidates(
533 IntegerVariable var) const {
534 const IntegerVariableSpan span = initial_candidates_[var];
535 if (span.size == 0) return absl::Span<const IntegerVariable>();
536 return absl::Span<const IntegerVariable>(&shared_buffer_[span.start],
537 span.size);
538}
539
540absl::Span<const IntegerVariable> VarDomination::DominatingVariables(
541 int ref) const {
543}
544
545absl::Span<const IntegerVariable> VarDomination::DominatingVariables(
546 IntegerVariable var) const {
547 const IntegerVariableSpan span = dominating_vars_[var];
548 if (span.size == 0) return absl::Span<const IntegerVariable>();
549 return absl::Span<const IntegerVariable>(&buffer_[span.start], span.size);
550}
551
552std::string VarDomination::DominationDebugString(IntegerVariable var) const {
553 const int ref = IntegerVariableToRef(var);
554 std::string result =
555 absl::StrCat(PositiveRef(ref), RefIsPositive(ref) ? "--" : "++", " : ");
556 for (const IntegerVariable dom : DominatingVariables(var)) {
557 const int dom_ref = IntegerVariableToRef(dom);
558 absl::StrAppend(&result, PositiveRef(dom_ref),
559 RefIsPositive(dom_ref) ? "++" : "--", " ");
560 }
561 return result;
562}
563
564// TODO(user): No need to set locking_ct_index_[var] if num_locks_[var] > 1
565void DualBoundStrengthening::CannotDecrease(absl::Span<const int> refs,
566 int ct_index) {
567 // Optim: We cache pointers to avoid refetching them in the loop.
568 IntegerValue* bounds = can_freely_decrease_until_.data();
569 int* locks = num_locks_.data();
570 int* locking_index = locking_ct_index_.data();
571 for (const int ref : refs) {
572 const int var = RefToIntegerVariable(ref).value();
573 bounds[var] = kMaxIntegerValue;
574 locks[var]++;
575 locking_index[var] = ct_index;
576 }
577}
578
579void DualBoundStrengthening::CannotIncrease(absl::Span<const int> refs,
580 int ct_index) {
581 // Optim: We cache pointers to avoid refetching them in the loop.
582 IntegerValue* bounds = can_freely_decrease_until_.data();
583 int* locks = num_locks_.data();
584 int* locking_index = locking_ct_index_.data();
585 for (const int ref : refs) {
586 const int negated_var = NegationOf(RefToIntegerVariable(ref)).value();
587 bounds[negated_var] = kMaxIntegerValue;
588 locks[negated_var]++;
589 locking_index[negated_var] = ct_index;
590 }
591}
592
593void DualBoundStrengthening::CannotMove(absl::Span<const int> refs,
594 int ct_index) {
595 // Optim: We cache pointers to avoid refetching them in the loop.
596 IntegerValue* bounds = can_freely_decrease_until_.data();
597 int* locks = num_locks_.data();
598 int* locking_index = locking_ct_index_.data();
599 for (const int ref : refs) {
600 const IntegerVariable var = RefToIntegerVariable(ref);
601 const IntegerVariable minus_var = NegationOf(var);
602 bounds[var.value()] = kMaxIntegerValue;
603 bounds[minus_var.value()] = kMaxIntegerValue;
604 locks[var.value()]++;
605 locks[minus_var.value()]++;
606 locking_index[var.value()] = ct_index;
607 locking_index[minus_var.value()] = ct_index;
608 }
609}
610
611template <typename LinearProto>
613 bool is_objective, const PresolveContext& context,
614 const LinearProto& linear, int64_t min_activity, int64_t max_activity,
615 int ct_index) {
616 const int64_t lb_limit = linear.domain(linear.domain_size() - 2);
617 const int64_t ub_limit = linear.domain(1);
618 const int num_terms = linear.vars_size();
619 for (int i = 0; i < num_terms; ++i) {
620 int ref = linear.vars(i);
621 int64_t coeff = linear.coeffs(i);
622 if (coeff < 0) {
623 ref = NegatedRef(ref);
624 coeff = -coeff;
625 }
626
627 const int64_t min_term = coeff * context.MinOf(ref);
628 const int64_t max_term = coeff * context.MaxOf(ref);
629 const int64_t term_diff = max_term - min_term;
630 const IntegerVariable var = RefToIntegerVariable(ref);
631
632 // lb side.
633 if (min_activity < lb_limit) {
634 num_locks_[var]++;
635 locking_ct_index_[var] = ct_index;
636 if (min_activity + term_diff < lb_limit) {
637 can_freely_decrease_until_[var] = kMaxIntegerValue;
638 } else {
639 const IntegerValue slack(lb_limit - min_activity);
640 const IntegerValue var_diff =
641 CeilRatio(IntegerValue(slack), IntegerValue(coeff));
642 can_freely_decrease_until_[var] =
643 std::max(can_freely_decrease_until_[var],
644 IntegerValue(context.MinOf(ref)) + var_diff);
645 }
646 }
647
648 if (is_objective) {
649 // We never want to increase the objective value. Note that if the
650 // objective is lower bounded, we checked that on the lb side above.
651 num_locks_[NegationOf(var)]++;
652 can_freely_decrease_until_[NegationOf(var)] = kMaxIntegerValue;
653 continue;
654 }
655
656 // ub side.
657 if (max_activity > ub_limit) {
658 num_locks_[NegationOf(var)]++;
659 locking_ct_index_[NegationOf(var)] = ct_index;
660 if (max_activity - term_diff > ub_limit) {
661 can_freely_decrease_until_[NegationOf(var)] = kMaxIntegerValue;
662 } else {
663 const IntegerValue slack(max_activity - ub_limit);
664 const IntegerValue var_diff =
665 CeilRatio(IntegerValue(slack), IntegerValue(coeff));
666 can_freely_decrease_until_[NegationOf(var)] =
667 std::max(can_freely_decrease_until_[NegationOf(var)],
668 -IntegerValue(context.MaxOf(ref)) + var_diff);
669 }
670 }
671 }
672}
673
674namespace {
675
676// This is used to detect if two linear constraint are equivalent if the literal
677// ref is mapped to another value. We fill a vector that will only be equal
678// to another such vector if the two constraint differ only there.
679void TransformLinearWithSpecialBoolean(const ConstraintProto& ct, int ref,
680 std::vector<int64_t>* output) {
682 output->clear();
683
684 // Deal with enforcement.
685 // We only detect NegatedRef() here.
686 if (!ct.enforcement_literal().empty()) {
687 output->push_back(ct.enforcement_literal().size());
688 for (const int literal : ct.enforcement_literal()) {
689 if (literal == NegatedRef(ref)) {
690 output->push_back(std::numeric_limits<int32_t>::max()); // Sentinel
691 } else {
692 output->push_back(literal);
693 }
694 }
695 }
696
697 // Deal with linear part.
698 // We look for both literal and not(literal) here.
699 int64_t offset = 0;
700 output->push_back(ct.linear().vars().size());
701 for (int i = 0; i < ct.linear().vars().size(); ++i) {
702 const int v = ct.linear().vars(i);
703 const int64_t c = ct.linear().coeffs(i);
704 if (v == ref) {
705 output->push_back(std::numeric_limits<int32_t>::max()); // Sentinel
706 output->push_back(c);
707 } else if (v == NegatedRef(ref)) {
708 // c * v = -c * (1 - v) + c
709 output->push_back(std::numeric_limits<int32_t>::max()); // Sentinel
710 output->push_back(-c);
711 offset += c;
712 } else {
713 output->push_back(v);
714 output->push_back(c);
715 }
716 }
717
718 // Domain.
719 for (const int64_t value : ct.linear().domain()) {
720 output->push_back(CapSub(value, offset));
721 }
722}
723
724} // namespace
725
727 SolutionCrush& crush = context->solution_crush();
728 num_deleted_constraints_ = 0;
729 const CpModelProto& cp_model = *context->working_model;
730 const int num_vars = cp_model.variables_size();
731 int64_t num_fixed_vars = 0;
732 for (int var = 0; var < num_vars; ++var) {
733 if (context->IsFixed(var)) continue;
734
735 // Fix to lb?
736 const int64_t lb = context->MinOf(var);
737 const int64_t ub_limit = std::max(lb, CanFreelyDecreaseUntil(var));
738 if (ub_limit == lb) {
739 ++num_fixed_vars;
740 CHECK(context->IntersectDomainWith(var, Domain(lb)));
741 continue;
742 }
743
744 // Fix to ub?
745 const int64_t ub = context->MaxOf(var);
746 const int64_t lb_limit =
747 std::min(ub, -CanFreelyDecreaseUntil(NegatedRef(var)));
748 if (lb_limit == ub) {
749 ++num_fixed_vars;
750 CHECK(context->IntersectDomainWith(var, Domain(ub)));
751 continue;
752 }
753
754 // Here we can fix to any value in [ub_limit, lb_limit] that is compatible
755 // with the current domain. We prefer zero or the lowest possible magnitude.
756 if (lb_limit > ub_limit) {
757 const Domain domain =
758 context->DomainOf(var).IntersectionWith(Domain(ub_limit, lb_limit));
759 if (!domain.IsEmpty()) {
760 int64_t value = domain.Contains(0) ? 0 : domain.Min();
761 if (value != 0) {
762 for (const int64_t bound : domain.FlattenedIntervals()) {
763 if (std::abs(bound) < std::abs(value)) value = bound;
764 }
765 }
766 ++num_fixed_vars;
767 context->UpdateRuleStats("dual: fix variable with multiple choices");
768 CHECK(context->IntersectDomainWith(var, Domain(value)));
769 continue;
770 }
771 }
772
773 // Here we can reduce the domain, but we must be careful when the domain
774 // has holes.
775 if (lb_limit > lb || ub_limit < ub) {
776 const int64_t new_ub =
777 ub_limit < ub
778 ? context->DomainOf(var)
780 Domain(ub_limit, std::numeric_limits<int64_t>::max()))
781 .Min()
782 : ub;
783 const int64_t new_lb =
784 lb_limit > lb
785 ? context->DomainOf(var)
787 Domain(std::numeric_limits<int64_t>::min(), lb_limit))
788 .Max()
789 : lb;
790 context->UpdateRuleStats("dual: reduced domain");
791 CHECK(context->IntersectDomainWith(var, Domain(new_lb, new_ub)));
792 }
793 }
794 if (num_fixed_vars > 0) {
795 context->UpdateRuleStats("dual: fix variable", num_fixed_vars);
796 }
797
798 // For detecting near-duplicate constraint that can be made equivalent.
799 // hash -> (ct_index, modified ref).
800 absl::flat_hash_set<int> equiv_ct_index_set;
801 absl::flat_hash_map<uint64_t, std::pair<int, int>> equiv_modified_constraints;
802 std::vector<int64_t> temp_data;
803 std::vector<int64_t> other_temp_data;
804 std::string s;
805
806 // If there is only one blocking constraint, we can simplify the problem in
807 // a few situation.
808 //
809 // TODO(user): Cover all the cases.
810 std::vector<bool> processed(num_vars, false);
811 int64_t num_bool_in_near_duplicate_ct = 0;
812 for (IntegerVariable var(0); var < num_locks_.size(); ++var) {
813 const int ref = VarDomination::IntegerVariableToRef(var);
814 const int positive_ref = PositiveRef(ref);
815 if (processed[positive_ref]) continue;
816 if (context->IsFixed(positive_ref)) continue;
817 if (context->VariableIsNotUsedAnymore(positive_ref)) continue;
818 if (context->VariableWasRemoved(positive_ref)) continue;
819
820 if (num_locks_[var] != 1) continue;
821 if (locking_ct_index_[var] == -1) {
822 context->UpdateRuleStats(
823 "TODO dual: only one unspecified blocking constraint?");
824 continue;
825 }
826
827 const int ct_index = locking_ct_index_[var];
828 const ConstraintProto& ct = context->working_model->constraints(ct_index);
830 // TODO(user): Fix variable right away rather than waiting for next call.
831 continue;
832 }
834 context->UpdateRuleStats("TODO dual: tighten at most one");
835 continue;
836 }
837
839 // If we have an enforcement literal then we can always add the
840 // implication "not enforced" => var at its lower bound.
841 // If we also have enforced => fixed var, then var is in affine relation
842 // with the enforced literal and we can remove one variable.
843 //
844 // TODO(user): We can also deal with more than one enforcement.
845 if (ct.enforcement_literal().size() == 1 &&
846 PositiveRef(ct.enforcement_literal(0)) != positive_ref) {
847 const int enf = ct.enforcement_literal(0);
848 const int64_t bound = RefIsPositive(ref) ? context->MinOf(positive_ref)
849 : context->MaxOf(positive_ref);
850 const Domain implied =
851 context->DomainOf(positive_ref)
853 context->deductions.ImpliedDomain(enf, positive_ref));
854 if (implied.IsEmpty()) {
855 context->UpdateRuleStats("dual: fix variable");
856 if (!context->SetLiteralToFalse(enf)) return false;
857 if (!context->IntersectDomainWith(positive_ref, Domain(bound))) {
858 return false;
859 }
860 continue;
861 }
862 if (implied.IsFixed()) {
863 // Corner case.
864 if (implied.FixedValue() == bound) {
865 context->UpdateRuleStats("dual: fix variable");
866 if (!context->IntersectDomainWith(positive_ref, implied)) {
867 return false;
868 }
869 continue;
870 }
871
872 // Note(user): If we have enforced => var fixed, we could actually
873 // just have removed var from the constraint it was implied by
874 // another constraint. If not, because of the new affine relation we
875 // could remove it right away.
876 processed[PositiveRef(enf)] = true;
877 processed[positive_ref] = true;
878 context->UpdateRuleStats("dual: affine relation");
879 // The new affine relation added below can break the hint if hint(enf)
880 // is 0. In this case the only constraint blocking `ref` from
881 // decreasing [`ct` = enf => (var = implied)] does not apply. We can
882 // thus set the hint of `positive_ref` to `bound` to preserve the hint
883 // feasibility.
884 crush.SetVarToValueIf(positive_ref, bound, NegatedRef(enf));
885 if (RefIsPositive(enf)) {
886 // positive_ref = enf * implied + (1 - enf) * bound.
887 if (!context->StoreAffineRelation(
888 positive_ref, enf, implied.FixedValue() - bound, bound)) {
889 return false;
890 }
891 } else {
892 // enf_var = PositiveRef(enf).
893 // positive_ref = (1 - enf_var) * implied + enf_var * bound.
894 if (!context->StoreAffineRelation(positive_ref, PositiveRef(enf),
895 bound - implied.FixedValue(),
896 implied.FixedValue())) {
897 return false;
898 }
899 }
900 continue;
901 }
902
903 if (context->CanBeUsedAsLiteral(positive_ref)) {
904 // If we have a literal, we always add the implication.
905 // This seems like a good thing to do.
906 processed[PositiveRef(enf)] = true;
907 processed[positive_ref] = true;
908 context->UpdateRuleStats("dual: add implication");
909 // The new implication can break the hint if hint(enf) is 0 and
910 // hint(ref) is 1. In this case the only locking constraint `ct` does
911 // not apply and thus does not prevent decreasing the hint of ref in
912 // order to preserve the hint feasibility.
913 crush.SetLiteralToValueIf(ref, false, NegatedRef(enf));
914 context->AddImplication(NegatedRef(enf), NegatedRef(ref));
916 continue;
917 }
918
919 // We can add an implication not_enforced => var to its bound ?
920 context->UpdateRuleStats("TODO dual: add implied bound");
921 }
922
923 // We can make enf equivalent to the constraint instead of just =>. This
924 // seems useful since internally we always use fully reified encoding.
926 ct.linear().vars().size() == 1 &&
927 ct.enforcement_literal().size() == 1 &&
928 ct.enforcement_literal(0) == NegatedRef(ref)) {
929 const int var = ct.linear().vars(0);
930 const Domain var_domain = context->DomainOf(var);
931 const Domain rhs = ReadDomainFromProto(ct.linear())
933 .IntersectionWith(var_domain);
934 if (rhs.IsEmpty()) {
935 context->UpdateRuleStats("linear1: infeasible");
936 if (!context->SetLiteralToFalse(ct.enforcement_literal(0))) {
937 return false;
938 }
939 processed[PositiveRef(ref)] = true;
940 processed[PositiveRef(var)] = true;
941 context->working_model->mutable_constraints(ct_index)->Clear();
942 context->UpdateConstraintVariableUsage(ct_index);
943 continue;
944 }
945 if (rhs == var_domain) {
946 context->UpdateRuleStats("linear1: always true");
947 processed[PositiveRef(ref)] = true;
948 processed[PositiveRef(var)] = true;
949 context->working_model->mutable_constraints(ct_index)->Clear();
950 context->UpdateConstraintVariableUsage(ct_index);
951 continue;
952 }
953
954 const Domain complement = rhs.Complement().IntersectionWith(var_domain);
955 if (rhs.IsFixed() || complement.IsFixed()) {
956 context->UpdateRuleStats("dual: make encoding equiv");
957 const int64_t value =
958 rhs.IsFixed() ? rhs.FixedValue() : complement.FixedValue();
959
960 int encoding_lit;
961 bool has_encoding = false;
962 if (!context->VarCanTakeValue(var, value)) {
963 encoding_lit = context->GetFalseLiteral();
964 has_encoding = true;
965 } else {
966 has_encoding =
967 context->HasVarValueEncoding(var, value, &encoding_lit);
968 }
969
970 if (has_encoding) {
971 // If it is different, we have an equivalence now, and we can
972 // remove the constraint.
973 if (rhs.IsFixed()) {
974 if (encoding_lit == NegatedRef(ref)) continue;
975 // Extending `ct` = "not(ref) => encoding_lit" to an equality can
976 // break the hint only if hint(ref) = hint(encoding_lit) = 1. But
977 // in this case `ct` is actually not blocking ref from decreasing.
978 // We can thus set its hint to 0 to preserve the hint feasibility.
979 crush.SetLiteralToValueIf(ref, false, encoding_lit);
980 if (!context->StoreBooleanEqualityRelation(encoding_lit,
981 NegatedRef(ref))) {
982 return false;
983 }
984 } else {
985 if (encoding_lit == ref) continue;
986 // Extending `ct` = "not(ref) => not(encoding_lit)" to an equality
987 // can break the hint only if hint(encoding_lit) = 0 and hint(ref)
988 // = 1. But in this case `ct` is actually not blocking ref from
989 // decreasing. We can thus set its hint to 0 to preserve the hint
990 // feasibility.
991 crush.SetLiteralToValueIf(ref, false, NegatedRef(encoding_lit));
992 if (!context->StoreBooleanEqualityRelation(encoding_lit, ref)) {
993 return false;
994 }
995 }
996 context->working_model->mutable_constraints(ct_index)->Clear();
997 context->UpdateConstraintVariableUsage(ct_index);
998 processed[PositiveRef(ref)] = true;
999 processed[PositiveRef(var)] = true;
1000 // `encoding_lit` was maybe a new variable added during this loop,
1001 // so make sure we cannot go out-of-bound.
1002 if (PositiveRef(encoding_lit) < processed.size()) {
1003 processed[PositiveRef(encoding_lit)] = true;
1004 }
1005 continue;
1006 }
1007
1008 processed[PositiveRef(ref)] = true;
1009 processed[PositiveRef(var)] = true;
1010 // The `new_ct` constraint `ref` => (`var` in `complement`) below can
1011 // break the hint if hint(var) is not in `complement`. In this case,
1012 // set the hint of `ref` to false. This should be safe since the only
1013 // constraint blocking `ref` from decreasing is `ct` = not(ref) =>
1014 // (`var` in `rhs`) -- which does not apply when `ref` is true.
1016 ref, false, {{var, 1}}, complement);
1017 ConstraintProto* new_ct = context->working_model->add_constraints();
1018 new_ct->add_enforcement_literal(ref);
1019 new_ct->mutable_linear()->add_vars(var);
1020 new_ct->mutable_linear()->add_coeffs(1);
1021 FillDomainInProto(complement, new_ct->mutable_linear());
1023
1024 if (rhs.IsFixed()) {
1025 context->StoreLiteralImpliesVarEqValue(NegatedRef(ref), var, value);
1026 context->StoreLiteralImpliesVarNeValue(ref, var, value);
1027 } else if (complement.IsFixed()) {
1028 context->StoreLiteralImpliesVarNeValue(NegatedRef(ref), var, value);
1029 context->StoreLiteralImpliesVarEqValue(ref, var, value);
1030 }
1032 continue;
1033 }
1034 }
1035
1036 // If we have two Booleans, each with a single blocking constraint that is
1037 // the same if we "swap" the Booleans reference, then we can make the
1038 // Booleans equivalent. This is because they will be forced to their bad
1039 // value only if it is needed by the other part of their respective
1040 // blocking constraints. This is related to the
1041 // FindAlmostIdenticalLinearConstraints() except that here we can deal
1042 // with non-equality or enforced constraints.
1043 //
1044 // TODO(user): Generalize to non-Boolean. Also for Boolean, we might
1045 // miss some possible reduction if replacing X by 1 - X make a constraint
1046 // near-duplicate of another.
1047 //
1048 // TODO(user): We can generalize to non-linear constraint.
1049 //
1050 // Optimization: Skip if this constraint was already used as a single
1051 // blocking constraint of another variable. If this is the case, it cannot
1052 // be equivalent to another constraint with "var" substituted, since
1053 // otherwise var would have two blocking constraint. We can safely skip
1054 // it. this make sure this is in O(num_entries) and not more.
1055 if (equiv_ct_index_set.insert(ct_index).second &&
1057 context->CanBeUsedAsLiteral(ref)) {
1058 TransformLinearWithSpecialBoolean(ct, ref, &temp_data);
1059 const uint64_t hash =
1060 fasthash64(temp_data.data(), temp_data.size() * sizeof(int64_t),
1061 uint64_t{0xa5b85c5e198ed849});
1062 const auto [it, inserted] =
1063 equiv_modified_constraints.insert({hash, {ct_index, ref}});
1064 if (!inserted) {
1065 // Already present!
1066 const auto [other_c_with_same_hash, other_ref] = it->second;
1067 CHECK_NE(other_c_with_same_hash, ct_index);
1068 const auto& other_ct =
1069 context->working_model->constraints(other_c_with_same_hash);
1070 TransformLinearWithSpecialBoolean(other_ct, other_ref,
1071 &other_temp_data);
1072 if (temp_data == other_temp_data) {
1073 // Corner case: We just discovered l => ct and not(l) => ct.
1074 // So ct must just always be true. And since that was the only
1075 // blocking constraint for l, we can just set l to an arbitrary
1076 // value.
1077 if (ref == NegatedRef(other_ref)) {
1078 context->UpdateRuleStats(
1079 "dual: detected l => ct and not(l) => ct with unused l!");
1080 if (!context->IntersectDomainWith(ref, Domain(0))) {
1081 return false;
1082 }
1083 continue;
1084 }
1085
1086 // We have a true equality. The two ref can be made equivalent.
1087 if (!processed[PositiveRef(other_ref)]) {
1088 ++num_bool_in_near_duplicate_ct;
1089 processed[PositiveRef(ref)] = true;
1090 processed[PositiveRef(other_ref)] = true;
1091 // If the two hints are different, and since both refs have an
1092 // equivalent blocking constraint, then the constraint is actually
1093 // not blocking the ref at 1 from decreasing. Hence we can set its
1094 // hint to false to preserve the hint feasibility despite the new
1095 // Boolean equality constraint.
1096 crush.UpdateLiteralsToFalseIfDifferent(ref, other_ref);
1097 if (!context->StoreBooleanEqualityRelation(ref, other_ref)) {
1098 return false;
1099 }
1100
1101 // We can delete one of the constraint since they are duplicate
1102 // now.
1103 ++num_deleted_constraints_;
1104 context->working_model->mutable_constraints(ct_index)->Clear();
1105 context->UpdateConstraintVariableUsage(ct_index);
1106 continue;
1107 }
1108 }
1109 }
1110 }
1111
1112 // Other potential cases?
1113 if (!ct.enforcement_literal().empty()) {
1115 ct.linear().vars().size() == 1 &&
1116 ct.enforcement_literal().size() == 1 &&
1117 ct.enforcement_literal(0) == NegatedRef(ref)) {
1118 context->UpdateRuleStats("TODO dual: make linear1 equiv");
1119 } else {
1120 context->UpdateRuleStats(
1121 "TODO dual: only one blocking enforced constraint?");
1122 }
1123 } else {
1124 context->UpdateRuleStats("TODO dual: only one blocking constraint?");
1125 }
1126 continue;
1127 }
1128 if (ct.enforcement_literal().size() != 1) continue;
1129
1130 // If (a => b) is the only constraint blocking a literal a in the up
1131 // direction, then we can set a == b !
1132 //
1133 // Recover a => b where a is having an unique up_lock (i.e this constraint).
1134 // Note that if many implications are encoded in the same bool_and, we have
1135 // to be careful that a is appearing in just one of them.
1136 //
1137 // TODO(user): Make sure implication graph is transitively reduced to not
1138 // miss such reduction. More generally, this might only use the graph rather
1139 // than the encoding into bool_and / at_most_one ? Basically if a =>
1140 // all_direct_deduction, we can transform it into a <=> all_direct_deduction
1141 // if that is interesting. This could always be done on a max-2sat problem
1142 // in one of the two direction. Also think about max-2sat specific presolve.
1143 int a = ct.enforcement_literal(0);
1144 int b = 1;
1145 if (PositiveRef(a) == positive_ref &&
1146 num_locks_[RefToIntegerVariable(NegatedRef(a))] == 1) {
1147 // Here, we can only add the equivalence if the literal is the only
1148 // on the lhs, otherwise there is actually more lock.
1149 if (ct.bool_and().literals().size() != 1) continue;
1150 b = ct.bool_and().literals(0);
1151 } else {
1152 bool found = false;
1154 for (const int lhs : ct.bool_and().literals()) {
1155 if (PositiveRef(lhs) == positive_ref &&
1156 num_locks_[RefToIntegerVariable(lhs)] == 1) {
1157 found = true;
1158 a = NegatedRef(lhs);
1159 break;
1160 }
1161 }
1162 CHECK(found);
1163 }
1164 CHECK_EQ(num_locks_[RefToIntegerVariable(NegatedRef(a))], 1);
1165
1166 processed[PositiveRef(a)] = true;
1167 processed[PositiveRef(b)] = true;
1168 // If hint(a) is false we can always set it to hint(b) since this can only
1169 // increase its value. If hint(a) is true then hint(b) must be true as well
1170 // if the hint is feasible, due to the a => b constraint. Setting hint(a) to
1171 // hint(b) is thus always safe. The opposite is true as well.
1172 crush.MakeLiteralsEqual(a, b);
1173 if (!context->StoreBooleanEqualityRelation(a, b)) return false;
1174 context->UpdateRuleStats("dual: enforced equivalence");
1175 }
1176
1177 if (num_bool_in_near_duplicate_ct) {
1178 context->UpdateRuleStats(
1179 "dual: equivalent Boolean in near-duplicate constraints",
1180 num_bool_in_near_duplicate_ct);
1181 }
1182
1183 VLOG(2) << "Num deleted constraints: " << num_deleted_constraints_;
1184 return true;
1185}
1186
1188 VarDomination* var_domination) {
1189 if (context.ModelIsUnsat()) return;
1190
1191 const CpModelProto& cp_model = *context.working_model;
1192 const int num_vars = cp_model.variables().size();
1193 var_domination->Reset(num_vars);
1194
1195 for (int var = 0; var < num_vars; ++var) {
1196 // Ignore variables that have been substituted already or are unused.
1197 if (context.IsFixed(var) || context.VariableWasRemoved(var) ||
1198 context.VariableIsNotUsedAnymore(var)) {
1199 var_domination->CanOnlyDominateEachOther({var});
1200 continue;
1201 }
1202
1203 // Deal with the affine relations that are not part of the proto.
1204 // Those only need to be processed in the first pass.
1205 const AffineRelation::Relation r = context.GetAffineRelation(var);
1206 if (r.representative != var) {
1207 if (r.coeff == 1) {
1208 var_domination->CanOnlyDominateEachOther(
1209 {NegatedRef(var), r.representative});
1210 } else if (r.coeff == -1) {
1211 var_domination->CanOnlyDominateEachOther({var, r.representative});
1212 } else {
1213 var_domination->CanOnlyDominateEachOther({var});
1214 var_domination->CanOnlyDominateEachOther({r.representative});
1215 }
1216 }
1217 }
1218
1219 // First scan: update the partition.
1220 const int num_constraints = cp_model.constraints_size();
1221 std::vector<bool> c_is_free_to_increase(num_constraints);
1222 std::vector<bool> c_is_free_to_decrease(num_constraints);
1223 for (int c = 0; c < num_constraints; ++c) {
1224 const ConstraintProto& ct = cp_model.constraints(c);
1225 switch (ct.constraint_case()) {
1227 break;
1229 break;
1231 break;
1233 var_domination->ActivityShouldNotChange(ct.exactly_one().literals(),
1234 /*coeffs=*/{});
1235 break;
1237 // TODO(user): Maybe we should avoid recomputing that here.
1238 const auto [min_activity, max_activity] =
1239 context.ComputeMinMaxActivity(ct.linear());
1240 const bool domain_is_simple = ct.linear().domain().size() == 2;
1241 const bool free_to_increase =
1242 domain_is_simple && ct.linear().domain(1) >= max_activity;
1243 const bool free_to_decrease =
1244 domain_is_simple && ct.linear().domain(0) <= min_activity;
1245 c_is_free_to_increase[c] = free_to_increase;
1246 c_is_free_to_decrease[c] = free_to_decrease;
1247 if (free_to_decrease && free_to_increase) break;
1248 if (!free_to_increase && !free_to_decrease) {
1249 var_domination->ActivityShouldNotChange(ct.linear().vars(),
1250 ct.linear().coeffs());
1251 }
1252 break;
1253 }
1254 default:
1255 // We cannot infer anything if we don't know the constraint.
1256 // TODO(user): Handle enforcement better here.
1257 for (const int var : context.ConstraintToVars(c)) {
1258 var_domination->CanOnlyDominateEachOther({var});
1259 }
1260 break;
1261 }
1262 }
1263
1264 // The objective is handled like a <= constraints, or an == constraint if
1265 // there is a non-trivial domain.
1266 if (cp_model.has_objective()) {
1267 // Important: We need to write the objective first to make sure it is up to
1268 // date.
1269 context.WriteObjectiveToProto();
1270 const auto [min_activity, max_activity] =
1271 context.ComputeMinMaxActivity(cp_model.objective());
1272 const auto& domain = cp_model.objective().domain();
1273 if (domain.empty() || (domain.size() == 2 && domain[0] <= min_activity)) {
1274 // do nothing for now.
1275 } else {
1276 var_domination->ActivityShouldNotChange(cp_model.objective().vars(),
1277 cp_model.objective().coeffs());
1278 }
1279 }
1280
1281 // Now do two more scan.
1282 // - the phase_ = 0 initialize candidate list, then EndFirstPhase()
1283 // - the phase_ = 1 filter them, then EndSecondPhase();
1284 std::vector<int> tmp;
1285 for (int phase = 0; phase < 2; phase++) {
1286 for (int c = 0; c < num_constraints; ++c) {
1287 const ConstraintProto& ct = cp_model.constraints(c);
1288 switch (ct.constraint_case()) {
1291 ct.bool_or().literals(),
1292 /*coeffs=*/{});
1293 break;
1295 // We process it like n clauses.
1296 //
1297 // TODO(user): the way we process that is a bit restrictive. By
1298 // working on the implication graph we could detect more dominance
1299 // relations. Since if a => b we say that a++ can only be paired with
1300 // b--, but it could actually be paired with any variables that when
1301 // dereased implies b = 0. This is a bit mitigated by the fact that
1302 // we regroup when we can such implications into big at most ones.
1303 tmp.clear();
1304 for (const int ref : ct.enforcement_literal()) {
1305 tmp.push_back(NegatedRef(ref));
1306 }
1307 for (const int ref : ct.bool_and().literals()) {
1308 tmp.push_back(ref);
1309 var_domination->ActivityShouldNotDecrease(/*enforcements=*/{}, tmp,
1310 /*coeffs=*/{});
1311 tmp.pop_back();
1312 }
1313 break;
1316 ct.at_most_one().literals(),
1317 /*coeffs=*/{});
1318 break;
1320 const bool free_to_increase = c_is_free_to_increase[c];
1321 const bool free_to_decrease = c_is_free_to_decrease[c];
1322 if (free_to_decrease && free_to_increase) break;
1323 if (free_to_increase) {
1325 ct.linear().vars(),
1326 ct.linear().coeffs());
1327 } else if (free_to_decrease) {
1329 ct.linear().vars(),
1330 ct.linear().coeffs());
1331 } else {
1332 if (!ct.enforcement_literal().empty()) {
1333 var_domination->ActivityShouldNotIncrease(
1334 /*enforcements=*/{}, ct.enforcement_literal(), /*coeffs=*/{});
1335 }
1336 }
1337 break;
1338 }
1339 default:
1340 break;
1341 }
1342 }
1343
1344 // The objective is handled like a <= constraints, or an == constraint if
1345 // there is a non-trivial domain.
1346 if (cp_model.has_objective()) {
1347 const auto [min_activity, max_activity] =
1348 context.ComputeMinMaxActivity(cp_model.objective());
1349 const auto& domain = cp_model.objective().domain();
1350 if (domain.empty() || (domain.size() == 2 && domain[0] <= min_activity)) {
1351 var_domination->ActivityShouldNotIncrease(
1352 /*enforcements=*/{}, cp_model.objective().vars(),
1353 cp_model.objective().coeffs());
1354 }
1355 }
1356
1357 if (phase == 0) {
1358 // Early abort if no possible relations can be found.
1359 //
1360 // TODO(user): We might be able to detect that nothing can be done earlier
1361 // during the constraint scanning.
1362 if (!var_domination->EndFirstPhase()) return;
1363 } else {
1364 CHECK_EQ(phase, 1);
1365 var_domination->EndSecondPhase();
1366 }
1367 }
1368
1369 // Some statistics.
1370 int64_t num_unconstrained_refs = 0;
1371 int64_t num_dominated_refs = 0;
1372 int64_t num_dominance_relations = 0;
1373 for (int var = 0; var < num_vars; ++var) {
1374 if (context.IsFixed(var)) continue;
1375
1376 for (const int ref : {var, NegatedRef(var)}) {
1377 if (var_domination->CanFreelyDecrease(ref)) {
1378 num_unconstrained_refs++;
1379 } else if (!var_domination->DominatingVariables(ref).empty()) {
1380 num_dominated_refs++;
1381 num_dominance_relations +=
1382 var_domination->DominatingVariables(ref).size();
1383 }
1384 }
1385 }
1386 if (num_unconstrained_refs == 0 && num_dominated_refs == 0) return;
1387 VLOG(1) << "Dominance:"
1388 << " num_unconstrained_refs=" << num_unconstrained_refs
1389 << " num_dominated_refs=" << num_dominated_refs
1390 << " num_dominance_relations=" << num_dominance_relations;
1391}
1392
1394 const PresolveContext& context,
1395 DualBoundStrengthening* dual_bound_strengthening) {
1396 if (context.ModelIsUnsat()) return;
1397 const CpModelProto& cp_model = *context.working_model;
1398 const int num_vars = cp_model.variables().size();
1399 dual_bound_strengthening->Reset(num_vars);
1400
1401 for (int var = 0; var < num_vars; ++var) {
1402 // Ignore variables that have been substituted already or are unused.
1403 if (context.IsFixed(var) || context.VariableWasRemoved(var) ||
1404 context.VariableIsNotUsedAnymore(var)) {
1405 dual_bound_strengthening->CannotMove({var});
1406 continue;
1407 }
1408
1409 // Deal with the affine relations that are not part of the proto.
1410 // Those only need to be processed in the first pass.
1411 const AffineRelation::Relation r = context.GetAffineRelation(var);
1412 if (r.representative != var) {
1413 dual_bound_strengthening->CannotMove({var, r.representative});
1414 }
1415 }
1416
1417 const int num_constraints = cp_model.constraints_size();
1418 for (int c = 0; c < num_constraints; ++c) {
1419 const ConstraintProto& ct = cp_model.constraints(c);
1420 dual_bound_strengthening->CannotIncrease(ct.enforcement_literal(), c);
1421 switch (ct.constraint_case()) {
1423 dual_bound_strengthening->CannotDecrease(ct.bool_or().literals(), c);
1424 break;
1426 dual_bound_strengthening->CannotDecrease(ct.bool_and().literals(), c);
1427 break;
1429 dual_bound_strengthening->CannotIncrease(ct.at_most_one().literals(),
1430 c);
1431 break;
1433 dual_bound_strengthening->CannotMove(ct.exactly_one().literals(), c);
1434 break;
1436 // TODO(user): Maybe we should avoid recomputing that here.
1437 const auto [min_activity, max_activity] =
1438 context.ComputeMinMaxActivity(ct.linear());
1439 dual_bound_strengthening->ProcessLinearConstraint(
1440 false, context, ct.linear(), min_activity, max_activity, c);
1441 break;
1442 }
1443 default:
1444 // We cannot infer anything if we don't know the constraint.
1445 // TODO(user): Handle enforcement better here.
1446 dual_bound_strengthening->CannotMove(context.ConstraintToVars(c), c);
1447 break;
1448 }
1449 }
1450
1451 // The objective is handled like a <= constraints, or an == constraint if
1452 // there is a non-trivial domain.
1453 if (cp_model.has_objective()) {
1454 // WARNING: The proto objective might not be up to date, so we need to
1455 // write it first.
1456 context.WriteObjectiveToProto();
1457 const auto [min_activity, max_activity] =
1458 context.ComputeMinMaxActivity(cp_model.objective());
1459 dual_bound_strengthening->ProcessLinearConstraint(
1460 true, context, cp_model.objective(), min_activity, max_activity);
1461 }
1462}
1463
1464namespace {
1465Domain DomainOfRef(const PresolveContext& context, int ref) {
1466 return RefIsPositive(ref) ? context.DomainOf(ref)
1467 : context.DomainOf(PositiveRef(ref)).Negation();
1468}
1469
1470// Decrements the solution hint of `ref` by the minimum amount necessary to be
1471// in `domain`, and increments the solution hint of one or more
1472// `dominating_variables` by the same total amount (or less if it is not
1473// possible to exactly match this amount).
1474//
1475// `domain` must be an interval with the same lower bound as `ref`'s current
1476// domain D in `context`, and whose upper bound must be in D.
1477void MaybeUpdateRefHintFromDominance(
1478 PresolveContext& context, int ref, const Domain& domain,
1479 absl::Span<const IntegerVariable> dominating_variables) {
1480 DCHECK_EQ(domain.NumIntervals(), 1);
1481 DCHECK_EQ(domain.Min(), DomainOfRef(context, ref).Min());
1482 DCHECK(DomainOfRef(context, ref).Contains(domain.Max()));
1483 std::vector<std::pair<int, Domain>> dominating_refs;
1484 dominating_refs.reserve(dominating_variables.size());
1485 for (const IntegerVariable var : dominating_variables) {
1486 const int dominating_ref = VarDomination::IntegerVariableToRef(var);
1487 dominating_refs.push_back(
1488 {dominating_ref, DomainOfRef(context, dominating_ref)});
1489 }
1490 context.solution_crush().UpdateRefsWithDominance(
1491 ref, domain.Min(), domain.Max(), dominating_refs);
1492}
1493
1494bool ProcessAtMostOne(
1495 absl::Span<const int> literals, absl::string_view message,
1496 const VarDomination& var_domination,
1497 util_intops::StrongVector<IntegerVariable, bool>* in_constraints,
1498 PresolveContext* context) {
1499 for (const int ref : literals) {
1500 (*in_constraints)[VarDomination::RefToIntegerVariable(ref)] = true;
1501 }
1502 for (const int ref : literals) {
1503 if (context->IsFixed(ref)) continue;
1504
1505 const auto dominating_ivars = var_domination.DominatingVariables(ref);
1506 for (const IntegerVariable ivar : dominating_ivars) {
1507 const int iref = VarDomination::IntegerVariableToRef(ivar);
1508 if (!(*in_constraints)[ivar]) continue;
1509 if (context->IsFixed(iref)) {
1510 continue;
1511 }
1512
1513 // We can set the dominated variable to false. If the hint value of `ref`
1514 // is 1 then the hint value of `iref` should be 0 due to the "at most one"
1515 // constraint. Hence the hint feasibility can always be preserved (if the
1516 // hint value of `ref` is 0 the hint does not need to be updated).
1517 context->UpdateRuleStats(message);
1518 context->solution_crush().UpdateLiteralsWithDominance(ref, iref);
1519 if (!context->SetLiteralToFalse(ref)) return false;
1520 break;
1521 }
1522 }
1523 for (const int ref : literals) {
1524 (*in_constraints)[VarDomination::RefToIntegerVariable(ref)] = false;
1525 }
1526 return true;
1527}
1528
1529} // namespace
1530
1531bool ExploitDominanceRelations(const VarDomination& var_domination,
1532 PresolveContext* context) {
1533 const CpModelProto& cp_model = *context->working_model;
1534 const int num_vars = cp_model.variables_size();
1535
1536 // Abort early if there is nothing to do.
1537 bool work_to_do = false;
1538 for (int var = 0; var < num_vars; ++var) {
1539 if (context->IsFixed(var)) continue;
1540 if (!var_domination.DominatingVariables(var).empty() ||
1541 !var_domination.DominatingVariables(NegatedRef(var)).empty()) {
1542 work_to_do = true;
1543 break;
1544 }
1545 }
1546 if (!work_to_do) return true;
1547
1548 const int64_t saved_num_operations = context->num_presolve_operations;
1549
1550 // Strenghtening via domination. When a variable is dominated by a bunch of
1551 // other, either we can do (var--, dom++) or if we can't (i.e all dominated
1552 // variable at their upper bound) then maybe all constraint are satisfied if
1553 // var is high enough and we can also decrease it.
1554 util_intops::StrongVector<IntegerVariable, int> can_freely_decrease_count(
1555 num_vars * 2, 0);
1557 num_vars * 2, std::numeric_limits<int64_t>::min());
1558
1559 // Temporary data that we fill/clear for each linear constraint.
1561 num_vars * 2, 0);
1562
1563 // Temporary data used for boolean constraints.
1564 util_intops::StrongVector<IntegerVariable, bool> in_constraints(num_vars * 2,
1565 false);
1566
1567 SolutionCrush& crush = context->solution_crush();
1568 absl::flat_hash_set<std::pair<int, int>> implications;
1569 const int num_constraints = cp_model.constraints_size();
1570 for (int c = 0; c < num_constraints; ++c) {
1571 const ConstraintProto& ct = cp_model.constraints(c);
1572
1574 if (ct.enforcement_literal().size() != 1) continue;
1575 const int a = ct.enforcement_literal(0);
1576 if (context->IsFixed(a)) continue;
1577 for (const int b : ct.bool_and().literals()) {
1578 if (context->IsFixed(b)) continue;
1579 implications.insert({a, b});
1580 implications.insert({NegatedRef(b), NegatedRef(a)});
1581
1582 // If (a--, b--) is valid, we can always set a to false. If the hint
1583 // value of `a` is 1 then the hint value of `b` should be 1 due to the
1584 // a => b constraint. Hence the hint feasibility can always be preserved
1585 // (if the hint value of `a` is 0 the hint does not need to be updated).
1586 for (const IntegerVariable ivar :
1587 var_domination.DominatingVariables(a)) {
1588 const int ref = VarDomination::IntegerVariableToRef(ivar);
1589 if (ref == NegatedRef(b)) {
1590 context->UpdateRuleStats("domination: in implication");
1591 crush.UpdateLiteralsWithDominance(a, ref);
1592 if (!context->SetLiteralToFalse(a)) return false;
1593 break;
1594 }
1595 }
1596 if (context->IsFixed(a)) break;
1597
1598 // If (b++, a++) is valid, then we can always set b to true. If the hint
1599 // value of `b` is 0 then the hint value of `a` should be 0 due to the
1600 // a => b constraint. Hence the hint feasibility can always be preserved
1601 // (if the hint value of `b` is 1 the hint does not need to be updated).
1602 for (const IntegerVariable ivar :
1603 var_domination.DominatingVariables(NegatedRef(b))) {
1604 const int ref = VarDomination::IntegerVariableToRef(ivar);
1605 if (ref == a) {
1606 context->UpdateRuleStats("domination: in implication");
1608 if (!context->SetLiteralToTrue(b)) return false;
1609 break;
1610 }
1611 }
1612 }
1613 continue;
1614 }
1615
1616 if (!ct.enforcement_literal().empty()) continue;
1617
1618 // TODO(user): More generally, combine with probing? if a dominated
1619 // variable implies one of its dominant to zero, then it can be set to
1620 // zero. It seems adding the implication below should have the same
1621 // effect? but currently it requires a lot of presolve rounds.
1622 const auto add_implications = [&implications](absl::Span<const int> lits) {
1623 if (lits.size() > 3) return;
1624 for (int i = 0; i < lits.size(); ++i) {
1625 for (int j = 0; j < lits.size(); ++j) {
1626 if (i == j) continue;
1627 implications.insert({lits[i], NegatedRef(lits[j])});
1628 }
1629 }
1630 };
1632 add_implications(ct.at_most_one().literals());
1633 if (!ProcessAtMostOne(ct.at_most_one().literals(),
1634 "domination: in at most one", var_domination,
1635 &in_constraints, context)) {
1636 return false;
1637 }
1638 } else if (ct.constraint_case() == ConstraintProto::kExactlyOne) {
1639 add_implications(ct.exactly_one().literals());
1640 if (!ProcessAtMostOne(ct.exactly_one().literals(),
1641 "domination: in exactly one", var_domination,
1642 &in_constraints, context)) {
1643 return false;
1644 }
1645 }
1646
1647 if (ct.constraint_case() != ConstraintProto::kLinear) continue;
1648
1649 int num_dominated = 0;
1650 for (const int var : context->ConstraintToVars(c)) {
1651 if (!var_domination.DominatingVariables(var).empty()) ++num_dominated;
1652 if (!var_domination.DominatingVariables(NegatedRef(var)).empty()) {
1653 ++num_dominated;
1654 }
1655 }
1656 if (num_dominated == 0) continue;
1657
1658 // Precompute.
1659 int64_t min_activity = 0;
1660 int64_t max_activity = 0;
1661 const int num_terms = ct.linear().vars_size();
1662 for (int i = 0; i < num_terms; ++i) {
1663 int ref = ct.linear().vars(i);
1664 int64_t coeff = ct.linear().coeffs(i);
1665 if (coeff < 0) {
1666 ref = NegatedRef(ref);
1667 coeff = -coeff;
1668 }
1669 const int64_t min_term = coeff * context->MinOf(ref);
1670 const int64_t max_term = coeff * context->MaxOf(ref);
1671 min_activity += min_term;
1672 max_activity += max_term;
1673 const IntegerVariable ivar = VarDomination::RefToIntegerVariable(ref);
1674 var_lb_to_ub_diff[ivar] = max_term - min_term;
1675 var_lb_to_ub_diff[NegationOf(ivar)] = min_term - max_term;
1676 }
1677 const int64_t rhs_lb = ct.linear().domain(0);
1678 const int64_t rhs_ub = ct.linear().domain(ct.linear().domain_size() - 1);
1679 if (max_activity < rhs_lb || min_activity > rhs_ub) {
1680 return context->NotifyThatModelIsUnsat("linear equation unsat.");
1681 }
1682
1683 // Returns the change magnitude in min-activity (resp. max-activity) if all
1684 // the given variables are fixed to their upper bound.
1685 const auto get_delta = [context, &var_lb_to_ub_diff](
1686 bool use_min_side,
1687 absl::Span<const IntegerVariable> vars) {
1688 int64_t delta = 0;
1689 for (const IntegerVariable var : vars) {
1690 // Tricky: For now we skip complex domain as we are not sure they
1691 // can be moved correctly.
1692 if (DomainOfRef(*context, VarDomination::IntegerVariableToRef(var))
1693 .NumIntervals() != 1) {
1694 continue;
1695 }
1696 if (use_min_side) {
1697 delta += std::max(int64_t{0}, var_lb_to_ub_diff[var]);
1698 } else {
1699 delta += std::max(int64_t{0}, -var_lb_to_ub_diff[var]);
1700 }
1701 }
1702 return delta;
1703 };
1704
1705 // Look for dominated var.
1706 for (int i = 0; i < num_terms; ++i) {
1707 const int ref = ct.linear().vars(i);
1708 const int64_t coeff = ct.linear().coeffs(i);
1709 const int64_t coeff_magnitude = std::abs(coeff);
1710 if (context->IsFixed(ref)) continue;
1711
1712 // For strenghtening using domination, just consider >= constraint.
1713 const bool only_lb = max_activity <= rhs_ub;
1714 const bool only_ub = min_activity >= rhs_lb;
1715 if (only_lb || only_ub) {
1716 // Always transform to coeff_magnitude * current_ref + ... >=
1717 const int current_ref = (coeff > 0) == only_lb ? ref : NegatedRef(ref);
1718 const int64_t shifted_rhs =
1719 only_lb ? rhs_lb - min_activity : max_activity - rhs_ub;
1720 const IntegerVariable current_ivar =
1722 can_freely_decrease_count[NegationOf(current_ivar)]++;
1723
1724 const int64_t delta = get_delta(
1725 only_lb, var_domination.DominatingVariables(current_ivar));
1726 if (delta > 0) {
1727 // When all dominated var are at their upper bound, we miss 'slack'
1728 // to make the constraint trivially satisfiable.
1729 const int64_t slack = shifted_rhs - delta;
1730 const int64_t current_lb = context->MinOf(current_ref);
1731
1732 // Any increase such that coeff * delta >= slack make the constraint
1733 // trivial.
1734 //
1735 // Note(user): It look like even if any of the upper bound of the
1736 // dominating var decrease, this should still be valid. Here we only
1737 // decrease such a bound due to a dominance relation, so the slack
1738 // when all dominating variable are at their bound should not really
1739 // decrease.
1740 const int64_t min_delta =
1741 slack <= 0 ? 0 : MathUtil::CeilOfRatio(slack, coeff_magnitude);
1742 can_freely_decrease_until[current_ivar] = std::max(
1743 can_freely_decrease_until[current_ivar], current_lb + min_delta);
1744 can_freely_decrease_count[current_ivar]++;
1745 }
1746 }
1747
1748 for (const int current_ref : {ref, NegatedRef(ref)}) {
1749 const absl::Span<const IntegerVariable> dominated_by =
1750 var_domination.DominatingVariables(current_ref);
1751 if (dominated_by.empty()) continue;
1752
1753 const bool ub_side = (coeff > 0) == (current_ref == ref);
1754 if (ub_side) {
1755 if (max_activity <= rhs_ub) continue;
1756 } else {
1757 if (min_activity >= rhs_lb) continue;
1758 }
1759 const int64_t slack =
1760 ub_side ? rhs_ub - min_activity : max_activity - rhs_lb;
1761
1762 // Compute the delta in min-activity if all dominating var moves to
1763 // their other bound.
1764 const int64_t delta = get_delta(ub_side, dominated_by);
1765 const int64_t lb = context->MinOf(current_ref);
1766 if (delta + coeff_magnitude > slack) {
1767 context->UpdateRuleStats("domination: fixed to lb.");
1768 const Domain reduced_domain = Domain(lb);
1769 MaybeUpdateRefHintFromDominance(*context, current_ref, reduced_domain,
1770 dominated_by);
1771 if (!context->IntersectDomainWith(current_ref, reduced_domain)) {
1772 return false;
1773 }
1774
1775 // We need to update the precomputed quantities.
1776 const IntegerVariable current_var =
1778 if (ub_side) {
1779 CHECK_GE(var_lb_to_ub_diff[current_var], 0);
1780 max_activity -= var_lb_to_ub_diff[current_var];
1781 } else {
1782 CHECK_LE(var_lb_to_ub_diff[current_var], 0);
1783 min_activity -= var_lb_to_ub_diff[current_var];
1784 }
1785 var_lb_to_ub_diff[current_var] = 0;
1786 var_lb_to_ub_diff[NegationOf(current_var)] = 0;
1787
1788 continue;
1789 }
1790
1791 const IntegerValue diff = FloorRatio(IntegerValue(slack - delta),
1792 IntegerValue(coeff_magnitude));
1793 int64_t new_ub = lb + diff.value();
1794 if (new_ub < context->MaxOf(current_ref)) {
1795 // Tricky: If there are holes, we can't just reduce the domain to
1796 // new_ub if it is not a valid value, so we need to compute the
1797 // Min() of the intersection.
1798 new_ub = DomainOfRef(*context, current_ref).ValueAtOrAfter(new_ub);
1799 }
1800 if (new_ub < context->MaxOf(current_ref)) {
1801 context->UpdateRuleStats("domination: reduced ub.");
1802 const Domain reduced_domain = Domain(lb, new_ub);
1803 MaybeUpdateRefHintFromDominance(*context, current_ref, reduced_domain,
1804 dominated_by);
1805 if (!context->IntersectDomainWith(current_ref, reduced_domain)) {
1806 return false;
1807 }
1808
1809 // We need to update the precomputed quantities.
1810 const IntegerVariable current_var =
1812 if (ub_side) {
1813 CHECK_GE(var_lb_to_ub_diff[current_var], 0);
1814 max_activity -= var_lb_to_ub_diff[current_var];
1815 } else {
1816 CHECK_LE(var_lb_to_ub_diff[current_var], 0);
1817 min_activity -= var_lb_to_ub_diff[current_var];
1818 }
1819 const int64_t new_diff = std::abs(coeff_magnitude * (new_ub - lb));
1820 if (ub_side) {
1821 var_lb_to_ub_diff[current_var] = new_diff;
1822 var_lb_to_ub_diff[NegationOf(current_var)] = -new_diff;
1823 max_activity += new_diff;
1824 } else {
1825 var_lb_to_ub_diff[current_var] = -new_diff;
1826 var_lb_to_ub_diff[NegationOf(current_var)] = +new_diff;
1827 min_activity -= new_diff;
1828 }
1829 }
1830 }
1831 }
1832
1833 // Restore.
1834 for (const int ref : ct.linear().vars()) {
1835 const IntegerVariable ivar = VarDomination::RefToIntegerVariable(ref);
1836 var_lb_to_ub_diff[ivar] = 0;
1837 var_lb_to_ub_diff[NegationOf(ivar)] = 0;
1838 }
1839 }
1840
1841 // For any dominance relation still left (i.e. between non-fixed vars), if
1842 // the variable are Boolean and X is dominated by Y, we can add
1843 // (X = 1) => (Y = 1). But, as soon as we do that, we break some symmetry
1844 // and cannot add any incompatible relations.
1845 //
1846 // EX: It is possible that X dominate Y and Y dominate X if they are both
1847 // appearing in exactly the same constraint with the same coefficient.
1848 //
1849 // TODO(user): if both variable are in a bool_or, this will allow us to
1850 // remove the dominated variable. Maybe we should exploit that to decide
1851 // which implication we add. Or just remove such variable and not add the
1852 // implications?
1853 //
1854 // TODO(user): generalize to non Booleans?
1855 int num_added = 0;
1857 2 * num_vars, false);
1858 for (int positive_ref = 0; positive_ref < num_vars; ++positive_ref) {
1859 if (context->IsFixed(positive_ref)) continue;
1860 if (context->VariableIsNotUsedAnymore(positive_ref)) continue;
1861 if (context->VariableWasRemoved(positive_ref)) continue;
1862
1863 // Increase the count for variable in the objective to account for the
1864 // kObjectiveConstraint in their VarToConstraints() list.
1865 if (!context->ObjectiveDomainIsConstraining()) {
1866 const int64_t obj_coeff = context->ObjectiveCoeff(positive_ref);
1867 if (obj_coeff > 0) {
1868 can_freely_decrease_count[VarDomination::RefToIntegerVariable(
1869 positive_ref)]++;
1870 } else if (obj_coeff < 0) {
1871 can_freely_decrease_count[NegationOf(
1872 VarDomination::RefToIntegerVariable(positive_ref))]++;
1873 }
1874 }
1875
1876 for (const int ref : {positive_ref, NegatedRef(positive_ref)}) {
1877 const IntegerVariable var = VarDomination::RefToIntegerVariable(ref);
1878 if (increase_is_forbidden[NegationOf(var)]) continue;
1879 if (can_freely_decrease_count[var] ==
1880 context->VarToConstraints(positive_ref).size()) {
1881 // We need to account for domain with hole, hence the ValueAtOrAfter().
1882 int64_t lb = can_freely_decrease_until[var];
1883 lb = DomainOfRef(*context, ref).ValueAtOrAfter(lb);
1884 if (lb < context->MaxOf(ref)) {
1885 // We have a candidate, however, we need to make sure the dominating
1886 // variable upper bound didn't change.
1887 //
1888 // TODO(user): It look like testing this is not really necessary.
1889 // The reduction done by this class seem to be order independent.
1890 bool ok = true;
1891 const absl::Span<const IntegerVariable> dominating_vars =
1892 var_domination.DominatingVariables(var);
1893 for (const IntegerVariable dom : dominating_vars) {
1894 // Note that we assumed that a fixed point was reached before this
1895 // is called, so modified_domains should have been empty as we
1896 // entered this function. If not, the code is still correct, but we
1897 // might miss some reduction, they will still likely be done later
1898 // though.
1899 if (increase_is_forbidden[dom] ||
1902 ok = false;
1903 break;
1904 }
1905 }
1906 if (increase_is_forbidden[NegationOf(var)]) {
1907 ok = false;
1908 }
1909 if (ok) {
1910 // TODO(user): Is this needed?
1911 increase_is_forbidden[var] = true;
1912 context->UpdateRuleStats(
1913 "domination: dual strenghtening using dominance");
1914 const Domain reduced_domain = Domain(context->MinOf(ref), lb);
1915 MaybeUpdateRefHintFromDominance(*context, ref, reduced_domain,
1916 dominating_vars);
1917 if (!context->IntersectDomainWith(ref, reduced_domain)) {
1918 return false;
1919 }
1920
1921 // The rest of the loop only care about Booleans.
1922 // And if this was boolean, we would have fixed it.
1923 // If it became Boolean, we wait for the next call.
1924 // TODO(user): maybe the last point can be improved.
1925 continue;
1926 }
1927 }
1928 }
1929
1930 if (!context->CanBeUsedAsLiteral(positive_ref)) continue;
1931 for (const IntegerVariable dom :
1932 var_domination.DominatingVariables(ref)) {
1933 if (increase_is_forbidden[dom]) continue;
1934 const int dom_ref = VarDomination::IntegerVariableToRef(dom);
1935 if (context->IsFixed(dom_ref)) continue;
1936 if (context->VariableIsNotUsedAnymore(dom_ref)) continue;
1937 if (context->VariableWasRemoved(dom_ref)) continue;
1938 if (!context->CanBeUsedAsLiteral(dom_ref)) continue;
1939 if (implications.contains({ref, dom_ref})) continue;
1940
1941 ++num_added;
1942 context->AddImplication(ref, dom_ref);
1943 // The newly added implication can break the hint only if the hint value
1944 // of `ref` is 1 and the hint value of `dom_ref` is 0. In this case the
1945 // call below fixes it by negating both values. Otherwise it does
1946 // nothing and thus preserves its feasibility.
1947 crush.UpdateLiteralsWithDominance(ref, dom_ref);
1949 implications.insert({ref, dom_ref});
1950 implications.insert({NegatedRef(dom_ref), NegatedRef(ref)});
1951
1952 // dom-- or var++ are now forbidden.
1953 increase_is_forbidden[var] = true;
1954 increase_is_forbidden[NegationOf(dom)] = true;
1955 }
1956 }
1957 }
1958
1959 if (num_added > 0) {
1960 VLOG(1) << "Added " << num_added << " domination implications.";
1961 context->UpdateRuleStats("domination: added implications", num_added);
1962 }
1963
1964 // TODO(user): We should probably be able to do something with this.
1965 if (saved_num_operations == context->num_presolve_operations) {
1966 context->UpdateRuleStats("TODO domination: unexploited dominations");
1967 }
1968
1969 return true;
1970}
1971
1972} // namespace sat
1973} // namespace operations_research
std::vector< int64_t > FlattenedIntervals() const
Domain IntersectionWith(const Domain &domain) const
bool Contains(int64_t value) const
Domain InverseMultiplicationBy(int64_t coeff) const
static IntegralType CeilOfRatio(IntegralType numerator, IntegralType denominator)
Definition mathutil.h:39
::int32_t enforcement_literal(int index) const
const ::operations_research::sat::BoolArgumentProto & exactly_one() const
const ::operations_research::sat::LinearConstraintProto & linear() const
const ::operations_research::sat::BoolArgumentProto & at_most_one() const
const ::operations_research::sat::BoolArgumentProto & bool_and() const
ABSL_ATTRIBUTE_REINITIALIZES void Clear() PROTOBUF_FINAL
const ::operations_research::sat::BoolArgumentProto & bool_or() const
::operations_research::sat::LinearConstraintProto *PROTOBUF_NONNULL mutable_linear()
::operations_research::sat::ConstraintProto *PROTOBUF_NONNULL mutable_constraints(int index)
const ::operations_research::sat::IntegerVariableProto & variables(int index) const
const ::operations_research::sat::ConstraintProto & constraints(int index) const
::operations_research::sat::ConstraintProto *PROTOBUF_NONNULL add_constraints()
const ::operations_research::sat::CpObjectiveProto & objective() const
Domain ImpliedDomain(int literal_ref, int var) const
void CannotIncrease(absl::Span< const int > refs, int ct_index=-1)
void CannotDecrease(absl::Span< const int > refs, int ct_index=-1)
void CannotMove(absl::Span< const int > refs, int ct_index=-1)
void ProcessLinearConstraint(bool is_objective, const PresolveContext &context, const LinearProto &linear, int64_t min_activity, int64_t max_activity, int ct_index=-1)
ABSL_MUST_USE_RESULT bool IntersectDomainWith(int ref, const Domain &domain, bool *domain_modified=nullptr)
bool StoreLiteralImpliesVarEqValue(int literal, int var, int64_t value)
bool StoreAffineRelation(int var_x, int var_y, int64_t coeff, int64_t offset, bool debug_no_recursion=false)
ABSL_MUST_USE_RESULT bool SetLiteralToTrue(int lit)
ABSL_MUST_USE_RESULT bool SetLiteralToFalse(int lit)
absl::Span< const int > ConstraintToVars(int c) const
ABSL_MUST_USE_RESULT bool NotifyThatModelIsUnsat(absl::string_view message="")
bool StoreBooleanEqualityRelation(int ref_a, int ref_b)
std::pair< int64_t, int64_t > ComputeMinMaxActivity(const ProtoWithVarsAndCoeffs &proto) const
bool HasVarValueEncoding(int ref, int64_t value, int *literal=nullptr)
AffineRelation::Relation GetAffineRelation(int ref) const
const absl::flat_hash_set< int > & VarToConstraints(int var) const
bool VarCanTakeValue(int var, int64_t value) const
bool StoreLiteralImpliesVarNeValue(int literal, int var, int64_t value)
void UpdateRuleStats(std::string_view name, int num_times=1)
void SetVarToValueIf(int var, int64_t value, int condition_lit)
void MakeLiteralsEqual(int lit1, int lit2)
void UpdateLiteralsWithDominance(int lit, int dominating_lit)
void SetLiteralToValueIfLinearConstraintViolated(int literal, bool value, absl::Span< const std::pair< int, int64_t > > linear, const Domain &domain)
void UpdateLiteralsToFalseIfDifferent(int lit1, int lit2)
void SetLiteralToValueIf(int literal, bool value, int condition_lit)
static int IntegerVariableToRef(IntegerVariable var)
absl::Span< const IntegerVariable > DominatingVariables(int ref) const
static IntegerVariable RefToIntegerVariable(int ref)
std::string DominationDebugString(IntegerVariable var) const
void CanOnlyDominateEachOther(absl::Span< const int > refs)
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)
void ActivityShouldNotDecrease(absl::Span< const int > enforcements, absl::Span< const int > refs, absl::Span< const int64_t > coeffs)
iterator insert(const_iterator pos, const value_type &x)
void STLClearObject(T *obj)
Definition stl_util.h:120
IntegerValue FloorRatio(IntegerValue dividend, IntegerValue positive_divisor)
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
bool ExploitDominanceRelations(const VarDomination &var_domination, PresolveContext *context)
IntegerValue CeilRatio(IntegerValue dividend, IntegerValue positive_divisor)
std::vector< IntegerVariable > NegationOf(absl::Span< const IntegerVariable > vars)
Definition integer.cc:52
void ScanModelForDualBoundStrengthening(const PresolveContext &context, DualBoundStrengthening *dual_bound_strengthening)
IntegerVariable PositiveVariable(IntegerVariable i)
void FillDomainInProto(const Domain &domain, ProtoWithDomain *proto)
Domain ReadDomainFromProto(const ProtoWithDomain &proto)
void ScanModelForDominanceDetection(PresolveContext &context, VarDomination *var_domination)
OR-Tools root namespace.
int64_t CapSub(int64_t x, int64_t y)
ClosedInterval::Iterator end(ClosedInterval interval)
uint64_t fasthash64(const void *buf, size_t len, uint64_t seed)
Definition hash.cc:36
const bool DEBUG_MODE
Definition radix_sort.h:266
std::string FormatCounter(int64_t num)
Definition logging.cc:30
Definition model.h:50
int64_t Max() const
Definition model.cc:346
int64_t Min() const
Definition model.cc:340