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