Google OR-Tools v9.9
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
lb_tree_search.cc
Go to the documentation of this file.
1// Copyright 2010-2024 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <algorithm>
17#include <cstdint>
18#include <functional>
19#include <memory>
20#include <string>
21#include <utility>
22#include <vector>
23
24#include "absl/log/check.h"
25#include "absl/strings/str_cat.h"
26#include "absl/time/clock.h"
27#include "absl/time/time.h"
28#include "absl/types/span.h"
32#include "ortools/sat/integer.h"
36#include "ortools/sat/model.h"
39#include "ortools/sat/sat_parameters.pb.h"
42#include "ortools/sat/util.h"
45
46namespace operations_research {
47namespace sat {
48
50 : time_limit_(model->GetOrCreate<TimeLimit>()),
51 random_(model->GetOrCreate<ModelRandomGenerator>()),
52 sat_solver_(model->GetOrCreate<SatSolver>()),
53 integer_encoder_(model->GetOrCreate<IntegerEncoder>()),
54 trail_(model->GetOrCreate<Trail>()),
55 integer_trail_(model->GetOrCreate<IntegerTrail>()),
56 watcher_(model->GetOrCreate<GenericLiteralWatcher>()),
57 shared_response_(model->GetOrCreate<SharedResponseManager>()),
58 sat_decision_(model->GetOrCreate<SatDecisionPolicy>()),
59 search_helper_(model->GetOrCreate<IntegerSearchHelper>()),
60 parameters_(*model->GetOrCreate<SatParameters>()) {
61 // We should create this class only in the presence of an objective.
62 //
63 // TODO(user): Starts with an initial variable score for all variable in
64 // the objective at their minimum value? this should emulate the first step of
65 // the core approach and gives a similar bound.
66 const ObjectiveDefinition* objective = model->Get<ObjectiveDefinition>();
67 CHECK(objective != nullptr);
68 objective_var_ = objective->objective_var;
69
70 // Identify an LP with the same objective variable.
71 //
72 // TODO(user): if we have many independent LP, this will find nothing.
75 if (lp->ObjectiveVariable() == objective_var_) {
76 lp_constraint_ = lp;
77 }
78 }
79
80 // We use the normal SAT search but we will bump the variable activity
81 // slightly differently. In addition to the conflicts, we also bump it each
82 // time the objective lower bound increase in a sub-node.
83 search_heuristic_ = SequentialSearch(
86 model->GetOrCreate<SearchHeuristics>()->fixed_search, model)});
87}
88
89void LbTreeSearch::UpdateParentObjective(int level) {
90 CHECK_GE(level, 0);
91 CHECK_LT(level, current_branch_.size());
92 if (level == 0) return;
93 const NodeIndex parent_index = current_branch_[level - 1];
94 Node& parent = nodes_[parent_index];
95 const NodeIndex child_index = current_branch_[level];
96 const Node& child = nodes_[child_index];
97 if (parent.true_child == child_index) {
98 parent.UpdateTrueObjective(child.MinObjective());
99 } else {
100 CHECK_EQ(parent.false_child, child_index);
101 parent.UpdateFalseObjective(child.MinObjective());
102 }
103}
104
105void LbTreeSearch::UpdateObjectiveFromParent(int level) {
106 CHECK_GE(level, 0);
107 CHECK_LT(level, current_branch_.size());
108 if (level == 0) return;
109 const NodeIndex parent_index = current_branch_[level - 1];
110 const Node& parent = nodes_[parent_index];
111 CHECK_GE(parent.MinObjective(), current_objective_lb_);
112 const NodeIndex child_index = current_branch_[level];
113 Node& child = nodes_[child_index];
114 if (parent.true_child == child_index) {
115 child.UpdateObjective(parent.true_objective);
116 } else {
117 CHECK_EQ(parent.false_child, child_index);
118 child.UpdateObjective(parent.false_objective);
119 }
120}
121
122void LbTreeSearch::DebugDisplayTree(NodeIndex root) const {
123 int num_nodes = 0;
124 const IntegerValue root_lb = nodes_[root].MinObjective();
125 const auto shifted_lb = [root_lb](IntegerValue lb) {
126 return std::max<int64_t>(0, (lb - root_lb).value());
127 };
128
129 absl::StrongVector<NodeIndex, int> level(nodes_.size(), 0);
130 std::vector<NodeIndex> to_explore = {root};
131 while (!to_explore.empty()) {
132 NodeIndex n = to_explore.back();
133 to_explore.pop_back();
134
135 ++num_nodes;
136 const Node& node = nodes_[n];
137
138 std::string s(level[n], ' ');
139 absl::StrAppend(&s, "#", n.value());
140
141 if (node.true_child < nodes_.size()) {
142 absl::StrAppend(&s, " [t:#", node.true_child.value(), " ",
143 shifted_lb(node.true_objective), "]");
144 to_explore.push_back(node.true_child);
145 level[node.true_child] = level[n] + 1;
146 } else {
147 absl::StrAppend(&s, " [t:## ", shifted_lb(node.true_objective), "]");
148 }
149 if (node.false_child < nodes_.size()) {
150 absl::StrAppend(&s, " [f:#", node.false_child.value(), " ",
151 shifted_lb(node.false_objective), "]");
152 to_explore.push_back(node.false_child);
153 level[node.false_child] = level[n] + 1;
154 } else {
155 absl::StrAppend(&s, " [f:## ", shifted_lb(node.false_objective), "]");
156 }
157 LOG(INFO) << s;
158 }
159 LOG(INFO) << "num_nodes: " << num_nodes;
160}
161
162// Here we forgot the whole search tree and restart.
163//
164// The idea is that the heuristic has now more information so it will likely
165// take better decision which will result in a smaller overall tree.
166bool LbTreeSearch::FullRestart() {
167 ++num_full_restarts_;
168 num_decisions_taken_at_last_restart_ = num_decisions_taken_;
169 num_nodes_in_tree_ = 0;
170 nodes_.clear();
171 current_branch_.clear();
172 return sat_solver_->RestoreSolverToAssumptionLevel();
173}
174
175void LbTreeSearch::MarkAsDeletedNodeAndUnreachableSubtree(Node& node) {
176 --num_nodes_in_tree_;
177 node.is_deleted = true;
178 if (sat_solver_->Assignment().LiteralIsTrue(node.literal)) {
179 MarkSubtreeAsDeleted(node.false_child);
180 } else {
181 MarkSubtreeAsDeleted(node.true_child);
182 }
183}
184
185void LbTreeSearch::MarkSubtreeAsDeleted(NodeIndex root) {
186 std::vector<NodeIndex> to_delete{root};
187 for (int i = 0; i < to_delete.size(); ++i) {
188 const NodeIndex n = to_delete[i];
189 if (n >= nodes_.size()) continue;
190
191 --num_nodes_in_tree_;
192 nodes_[n].is_deleted = true;
193
194 to_delete.push_back(nodes_[n].true_child);
195 to_delete.push_back(nodes_[n].false_child);
196 }
197}
198
199std::string LbTreeSearch::SmallProgressString() const {
200 return absl::StrCat(
201 "nodes=", num_nodes_in_tree_, "/", nodes_.size(),
202 " rc=", num_rc_detected_, " decisions=", num_decisions_taken_,
203 " @root=", num_back_to_root_node_, " restarts=", num_full_restarts_);
204}
205
207 const std::function<void()>& feasible_solution_observer) {
208 if (!sat_solver_->RestoreSolverToAssumptionLevel()) {
209 return sat_solver_->UnsatStatus();
210 }
211
212 // We currently restart the search tree from scratch from time to times:
213 // - Initially, every kNumDecisionsBeforeInitialRestarts, for at most
214 // kMaxNumInitialRestarts times.
215 // - Every time we backtrack to level zero, we count how many nodes are worse
216 // than the best known objective lower bound. If this is true for more than
217 // half of the existing nodes, we restart and clear all nodes. If if this
218 // happens during the initial restarts phase, it reset the above counter and
219 // uses 1 of the available initial restarts.
220 //
221 // This has 2 advantages:
222 // - It allows our "pseudo-cost" to kick in and experimentally result in
223 // smaller trees down the road.
224 // - It removes large inefficient search trees.
225 //
226 // TODO(user): a strong branching initial start, or allowing a few decision
227 // per nodes might be a better approach.
228 //
229 // TODO(user): It would also be cool to exploit the reason for the LB increase
230 // even more.
231 const int kMaxNumInitialRestarts = 10;
232 const int64_t kNumDecisionsBeforeInitialRestarts = 1000;
233
234 while (!time_limit_->LimitReached() && !shared_response_->ProblemIsSolved()) {
235 // This is the current bound we try to improve. We cache it here to avoid
236 // getting the lock many times and it is also easier to follow the code if
237 // this is assumed constant for one iteration.
238 current_objective_lb_ = shared_response_->GetInnerObjectiveLowerBound();
239
240 // If some branches already have a good lower bound, no need to call the LP
241 // on those.
242 watcher_->SetStopPropagationCallback([this] {
243 return integer_trail_->LowerBound(objective_var_) > current_objective_lb_;
244 });
245
246 // Propagate upward in the tree the new objective lb.
247 if (!current_branch_.empty()) {
248 // Our branch is always greater or equal to the level.
249 // We increase the objective_lb of the current node if needed.
250 {
251 const int current_level = sat_solver_->CurrentDecisionLevel();
252 CHECK_GE(current_branch_.size(), current_level);
253 for (int i = 0; i < current_level; ++i) {
254 CHECK(sat_solver_->Assignment().LiteralIsAssigned(
255 nodes_[current_branch_[i]].literal));
256 }
257 if (current_level < current_branch_.size()) {
258 nodes_[current_branch_[current_level]].UpdateObjective(
259 integer_trail_->LowerBound(objective_var_));
260 }
261
262 // Minor optim: sometimes, because of the LP and cuts, the reason for
263 // objective_var_ only contains lower level literals, so we can exploit
264 // that.
265 //
266 // TODO(user): No point checking that if the objective lb wasn't
267 // assigned at this level.
268 //
269 // TODO(user): Exploit the reasons further.
270 if (integer_trail_->LowerBound(objective_var_) >
271 integer_trail_->LevelZeroLowerBound(objective_var_)) {
272 const std::vector<Literal> reason =
274 objective_var_, integer_trail_->LowerBound(objective_var_)));
275 int max_level = 0;
276 for (const Literal l : reason) {
277 max_level = std::max<int>(
278 max_level,
279 sat_solver_->LiteralTrail().Info(l.Variable()).level);
280 }
281 if (max_level < current_level) {
282 nodes_[current_branch_[max_level]].UpdateObjective(
283 integer_trail_->LowerBound(objective_var_));
284 }
285 }
286 }
287
288 // Propagate upward and then forward any new bounds.
289 for (int level = current_branch_.size(); --level > 0;) {
290 UpdateParentObjective(level);
291 }
292 nodes_[current_branch_[0]].UpdateObjective(current_objective_lb_);
293 for (int level = 1; level < current_branch_.size(); ++level) {
294 UpdateObjectiveFromParent(level);
295 }
296
297 // If the root lb increased, update global shared objective lb.
298 const IntegerValue bound = nodes_[current_branch_[0]].MinObjective();
299 if (bound > current_objective_lb_) {
300 shared_response_->UpdateInnerObjectiveBounds(
301 absl::StrCat("lb_tree_search (", SmallProgressString(), ") "),
302 bound, integer_trail_->LevelZeroUpperBound(objective_var_));
303 current_objective_lb_ = bound;
304 if (VLOG_IS_ON(3)) DebugDisplayTree(current_branch_[0]);
305 }
306 }
307
308 // Each time we are back here, we bump the activities of the variable that
309 // are part of the objective lower bound reason.
310 //
311 // Note that this is why we prefer not to increase the lower zero lower
312 // bound of objective_var_ with the tree root lower bound, so we can exploit
313 // more reasons.
314 //
315 // TODO(user): This is slightly different than bumping each time we
316 // push a decision that result in an LB increase. This is also called on
317 // backjump for instance.
318 if (integer_trail_->LowerBound(objective_var_) >
319 integer_trail_->LevelZeroLowerBound(objective_var_)) {
320 std::vector<Literal> reason =
322 objective_var_, integer_trail_->LowerBound(objective_var_)));
323 sat_decision_->BumpVariableActivities(reason);
324 sat_decision_->UpdateVariableActivityIncrement();
325 }
326
327 // Forget the whole tree and restart.
328 // We will do it periodically at the beginning of the search each time we
329 // cross the kNumDecisionsBeforeInitialRestarts decision since the last
330 // restart. This will happen at most kMaxNumInitialRestarts times.
331 if (num_decisions_taken_ >= num_decisions_taken_at_last_restart_ +
332 kNumDecisionsBeforeInitialRestarts &&
333 num_full_restarts_ < kMaxNumInitialRestarts) {
334 VLOG(2) << "lb_tree_search (initial_restart " << SmallProgressString()
335 << ")";
336 if (!FullRestart()) return sat_solver_->UnsatStatus();
337 }
338
339 // Backtrack if needed.
340 //
341 // Our algorithm stop exploring a branch as soon as its objective lower
342 // bound is greater than the root lower bound. We then backtrack to the
343 // first node in the branch that is not yet closed under this bound.
344 //
345 // TODO(user): If we remember how far we can backjump for both true/false
346 // branch, we could be more efficient.
347 while (current_branch_.size() > sat_solver_->CurrentDecisionLevel() + 1 ||
348 (current_branch_.size() > 1 &&
349 nodes_[current_branch_.back()].MinObjective() >
350 current_objective_lb_)) {
351 current_branch_.pop_back();
352 }
353
354 // Backtrack the solver.
355 {
356 int backtrack_level =
357 std::max(0, static_cast<int>(current_branch_.size()) - 1);
358
359 // Periodic backtrack to level zero so we can import bounds.
360 if (num_decisions_taken_ >=
361 num_decisions_taken_at_last_level_zero_ + 10000) {
362 backtrack_level = 0;
363 }
364
365 sat_solver_->Backtrack(backtrack_level);
366 if (!sat_solver_->FinishPropagation()) {
367 return sat_solver_->UnsatStatus();
368 }
369 }
370
371 if (sat_solver_->CurrentDecisionLevel() == 0) {
372 ++num_back_to_root_node_;
373 num_decisions_taken_at_last_level_zero_ = num_decisions_taken_;
374 }
375
376 // This will import other workers bound if we are back to level zero.
377 if (!search_helper_->BeforeTakingDecision()) {
378 return sat_solver_->UnsatStatus();
379 }
380
381 // If the search has not just been restarted (in which case nodes_ would be
382 // empty), and if we are at level zero (either naturally, or if the
383 // backtrack level was set to zero in the above code), let's run a different
384 // heuristic to decide whether to restart the search from scratch or not.
385 //
386 // We ignore small search trees.
387 if (sat_solver_->CurrentDecisionLevel() == 0 && num_nodes_in_tree_ > 50) {
388 // Let's count how many nodes have worse objective bounds than the best
389 // known external objective lower bound.
390 const IntegerValue latest_lb =
391 shared_response_->GetInnerObjectiveLowerBound();
392 int num_nodes = 0;
393 int num_nodes_with_lower_objective = 0;
394 for (const Node& node : nodes_) {
395 if (node.is_deleted) continue;
396 ++num_nodes;
397 if (node.MinObjective() < latest_lb) num_nodes_with_lower_objective++;
398 }
399 DCHECK_EQ(num_nodes_in_tree_, num_nodes);
400 if (num_nodes_with_lower_objective * 2 > num_nodes) {
401 VLOG(2) << "lb_tree_search restart nodes: "
402 << num_nodes_with_lower_objective << "/" << num_nodes << " : "
403 << 100.0 * num_nodes_with_lower_objective / num_nodes << "%"
404 << ", decisions:" << num_decisions_taken_;
405 if (!FullRestart()) return sat_solver_->UnsatStatus();
406 }
407 }
408
409 // Dive: Follow the branch with lowest objective.
410 // Note that we do not creates new nodes here.
411 //
412 // TODO(user): If we have new information and our current objective bound
413 // is higher than any bound in a whole subtree, we might want to just
414 // restart this subtree exploration?
415 while (current_branch_.size() == sat_solver_->CurrentDecisionLevel() + 1) {
416 const int level = current_branch_.size() - 1;
417 CHECK_EQ(level, sat_solver_->CurrentDecisionLevel());
418 Node& node = nodes_[current_branch_[level]];
419 node.UpdateObjective(std::max(
420 current_objective_lb_, integer_trail_->LowerBound(objective_var_)));
421 if (node.MinObjective() > current_objective_lb_) break;
422 CHECK_EQ(node.MinObjective(), current_objective_lb_) << level;
423
424 // This will be set to the next node index.
425 NodeIndex n;
426
427 // If the variable is already fixed, we bypass the node and connect
428 // its parent directly to the relevant child.
429 if (sat_solver_->Assignment().LiteralIsAssigned(node.literal)) {
430 IntegerValue new_lb;
431 if (sat_solver_->Assignment().LiteralIsTrue(node.literal)) {
432 n = node.true_child;
433 new_lb = node.true_objective;
434 } else {
435 n = node.false_child;
436 new_lb = node.false_objective;
437 }
438 MarkAsDeletedNodeAndUnreachableSubtree(node);
439
440 // We jump directly to the subnode.
441 // Else we will change the root.
442 current_branch_.pop_back();
443 if (!current_branch_.empty()) {
444 const NodeIndex parent = current_branch_.back();
445 if (sat_solver_->Assignment().LiteralIsTrue(nodes_[parent].literal)) {
446 nodes_[parent].true_child = n;
447 nodes_[parent].UpdateTrueObjective(new_lb);
448 } else {
449 DCHECK(sat_solver_->Assignment().LiteralIsFalse(
450 nodes_[parent].literal));
451 nodes_[parent].false_child = n;
452 nodes_[parent].UpdateFalseObjective(new_lb);
453 }
454 if (nodes_[parent].MinObjective() > current_objective_lb_) break;
455 }
456 } else {
457 // See if we have better bounds using the current LP state.
458 ExploitReducedCosts(current_branch_[level]);
459 if (node.MinObjective() > current_objective_lb_) break;
460
461 // If both lower bound are the same, we pick the literal branch. We do
462 // that because this is the polarity that was chosen by the SAT
463 // heuristic in the first place. We tried random, it doesn't seems to
464 // work as well.
465 num_decisions_taken_++;
466 const bool choose_true = node.true_objective <= node.false_objective;
467 if (choose_true) {
468 n = node.true_child;
469 search_helper_->TakeDecision(node.literal);
470 } else {
471 n = node.false_child;
472 search_helper_->TakeDecision(node.literal.Negated());
473 }
474
475 // Conflict?
476 if (current_branch_.size() != sat_solver_->CurrentDecisionLevel()) {
477 if (choose_true) {
478 node.UpdateTrueObjective(kMaxIntegerValue);
479 } else {
480 node.UpdateFalseObjective(kMaxIntegerValue);
481 }
482 break;
483 }
484
485 // Update the proper field and abort the dive if we crossed the
486 // threshold.
487 const IntegerValue lb = integer_trail_->LowerBound(objective_var_);
488 if (choose_true) {
489 node.UpdateTrueObjective(lb);
490 } else {
491 node.UpdateFalseObjective(lb);
492 }
493 if (lb > current_objective_lb_) break;
494 }
495
496 if (VLOG_IS_ON(1)) {
497 shared_response_->LogMessageWithThrottling(
498 "TreeS", absl::StrCat(" (", SmallProgressString(), ")"));
499 }
500
501 if (n < nodes_.size()) {
502 current_branch_.push_back(n);
503 } else {
504 break;
505 }
506 }
507
508 // If a conflict occurred, we will backtrack.
509 if (current_branch_.size() != sat_solver_->CurrentDecisionLevel()) {
510 continue;
511 }
512
513 // This test allow to not take a decision when the branch is already closed
514 // (i.e. the true branch or false branch lb is high enough). Adding it
515 // basically changes if we take the decision later when we explore the
516 // branch or right now.
517 //
518 // I feel taking it later is better. It also avoid creating uneeded nodes.
519 // It does change the behavior on a few problem though. For instance on
520 // irp.mps.gz, the search works better without this, whatever the random
521 // seed. Not sure why, maybe it creates more diversity?
522 //
523 // Another difference is that if the search is done and we have a feasible
524 // solution, we will not report it because of this test (except if we are
525 // at the optimal).
526 if (integer_trail_->LowerBound(objective_var_) > current_objective_lb_) {
527 continue;
528 }
529
530 // We are about to take a new decision, what we will do is dive until
531 // the objective lower bound increase. we will then create a bunch of new
532 // nodes in the tree.
533 //
534 // By analyzing the reason for the increase, we can create less nodes than
535 // if we just followed the initial heuristic.
536 //
537 // TODO(user): In multithread, this change the behavior a lot since we
538 // dive until we beat the best shared bound. Maybe we shouldn't do that.
539 const int base_level = sat_solver_->CurrentDecisionLevel();
540 while (true) {
541 // TODO(user): We sometimes branch on the objective variable, this should
542 // probably be avoided.
543 if (sat_solver_->ModelIsUnsat()) return sat_solver_->UnsatStatus();
544 LiteralIndex decision;
545 if (!search_helper_->GetDecision(search_heuristic_, &decision)) {
546 continue;
547 }
548
549 // No new decision: search done.
550 if (time_limit_->LimitReached()) return SatSolver::LIMIT_REACHED;
551 if (decision == kNoLiteralIndex) {
552 feasible_solution_observer();
553 break;
554 }
555
556 num_decisions_taken_++;
557 if (!search_helper_->TakeDecision(Literal(decision))) {
558 return sat_solver_->UnsatStatus();
559 }
560 if (sat_solver_->CurrentDecisionLevel() < base_level) break;
561 if (integer_trail_->LowerBound(objective_var_) > current_objective_lb_) {
562 break;
563 }
564 }
565 if (sat_solver_->CurrentDecisionLevel() <= base_level) continue;
566
567 // Analyse the reason for objective increase. Deduce a set of new nodes to
568 // append to the tree.
569 //
570 // TODO(user): Try to minimize the number of decisions?
571 const std::vector<Literal> reason =
573 objective_var_, integer_trail_->LowerBound(objective_var_)));
574 std::vector<Literal> decisions = ExtractDecisions(base_level, reason);
575
576 // Bump activities.
577 sat_decision_->BumpVariableActivities(reason);
578 sat_decision_->BumpVariableActivities(decisions);
579 sat_decision_->UpdateVariableActivityIncrement();
580
581 // Create one node per new decisions.
582 CHECK_EQ(current_branch_.size(), base_level);
583 for (const Literal d : decisions) {
584 AppendNewNodeToCurrentBranch(d);
585 }
586
587 // Update the objective of the last node in the branch since we just
588 // improved that.
589 if (!current_branch_.empty()) {
590 Node& n = nodes_[current_branch_.back()];
591 if (sat_solver_->Assignment().LiteralIsTrue(n.literal)) {
592 n.UpdateTrueObjective(integer_trail_->LowerBound(objective_var_));
593 } else {
594 n.UpdateFalseObjective(integer_trail_->LowerBound(objective_var_));
595 }
596 }
597
598 // Reset the solver to a correct state since we have a subset of the
599 // current propagation. We backtrack as little as possible.
600 //
601 // The decision level is the number of decision taken.
602 // Decision()[level] is the decision at that level.
603 int backtrack_level = base_level;
604 CHECK_LE(current_branch_.size(), sat_solver_->CurrentDecisionLevel());
605 while (backtrack_level < current_branch_.size() &&
606 sat_solver_->Decisions()[backtrack_level].literal ==
607 nodes_[current_branch_[backtrack_level]].literal) {
608 ++backtrack_level;
609 }
610 sat_solver_->Backtrack(backtrack_level);
611
612 // Update bounds with reduced costs info.
613 //
614 // TODO(user): Uses old optimal constraint that we just potentially
615 // backtracked over?
616 //
617 // TODO(user): We could do all at once rather than in O(#decision * #size).
618 for (int i = backtrack_level; i < current_branch_.size(); ++i) {
619 ExploitReducedCosts(current_branch_[i]);
620 }
621 }
622
624}
625
626std::vector<Literal> LbTreeSearch::ExtractDecisions(
627 int base_level, absl::Span<const Literal> conflict) {
628 std::vector<int> num_per_level(sat_solver_->CurrentDecisionLevel() + 1, 0);
629 std::vector<bool> is_marked;
630 for (const Literal l : conflict) {
631 const AssignmentInfo& info = trail_->Info(l.Variable());
632 if (info.level <= base_level) continue;
633 num_per_level[info.level]++;
634 if (info.trail_index >= is_marked.size()) {
635 is_marked.resize(info.trail_index + 1);
636 }
637 is_marked[info.trail_index] = true;
638 }
639
640 std::vector<Literal> result;
641 if (is_marked.empty()) return result;
642 for (int i = is_marked.size() - 1; i >= 0; --i) {
643 if (!is_marked[i]) continue;
644
645 const Literal l = (*trail_)[i];
646 const AssignmentInfo& info = trail_->Info(l.Variable());
647 if (info.level <= base_level) break;
648 if (num_per_level[info.level] == 1) {
649 result.push_back(l);
650 continue;
651 }
652
653 // Expand.
654 num_per_level[info.level]--;
655 for (const Literal new_l : trail_->Reason(l.Variable())) {
656 const AssignmentInfo& new_info = trail_->Info(new_l.Variable());
657 if (new_info.level <= base_level) continue;
658 if (is_marked[new_info.trail_index]) continue;
659 is_marked[new_info.trail_index] = true;
660 num_per_level[new_info.level]++;
661 }
662 }
663
664 // We prefer to keep the same order.
665 std::reverse(result.begin(), result.end());
666 return result;
667}
668
669void LbTreeSearch::AppendNewNodeToCurrentBranch(Literal decision) {
670 const NodeIndex n(nodes_.size());
671 ++num_nodes_in_tree_;
672 nodes_.emplace_back(Literal(decision), current_objective_lb_);
673 if (!current_branch_.empty()) {
674 const NodeIndex parent = current_branch_.back();
675 if (sat_solver_->Assignment().LiteralIsTrue(nodes_[parent].literal)) {
676 nodes_[parent].true_child = n;
677 nodes_[parent].UpdateTrueObjective(nodes_.back().MinObjective());
678 } else {
679 CHECK(sat_solver_->Assignment().LiteralIsFalse(nodes_[parent].literal));
680 nodes_[parent].false_child = n;
681 nodes_[parent].UpdateFalseObjective(nodes_.back().MinObjective());
682 }
683 }
684 current_branch_.push_back(n);
685}
686
687// Looking at the reduced costs, we can already have a bound for one of the
688// branch. Increasing the corresponding objective can save some branches,
689// and also allow for a more incremental LP solving since we do less back
690// and forth.
691//
692// TODO(user): The code to recover that is a bit convoluted. Alternatively
693// Maybe we should do a "fast" propagation without the LP in each branch.
694// That will work as long as we keep these optimal LP constraints around
695// and propagate them.
696//
697// TODO(user): Incorporate this in the heuristic so we choose more Booleans
698// inside these LP explanations?
699void LbTreeSearch::ExploitReducedCosts(NodeIndex n) {
700 if (lp_constraint_ == nullptr) return;
701
702 // TODO(user): we could consider earlier constraints instead of just
703 // looking at the last one, but experiments didn't really show a big
704 // gain.
705 const auto& cts = lp_constraint_->OptimalConstraints();
706 if (cts.empty()) return;
707 const std::unique_ptr<IntegerSumLE128>& rc = cts.back();
708
709 // Note that this return literal EQUIVALENT to the node.literal, not just
710 // implied by it. We need that for correctness.
711 int num_tests = 0;
712 Node& node = nodes_[n];
713 CHECK(!sat_solver_->Assignment().LiteralIsAssigned(node.literal));
714 for (const IntegerLiteral integer_literal :
715 integer_encoder_->GetIntegerLiterals(node.literal)) {
716 // To avoid bad corner case. Not sure it ever triggers.
717 if (++num_tests > 10) break;
718
719 const std::pair<IntegerValue, IntegerValue> bounds =
720 rc->ConditionalLb(integer_literal, objective_var_);
721 if (bounds.first > node.false_objective) {
722 ++num_rc_detected_;
723 node.UpdateFalseObjective(bounds.first);
724 }
725 if (bounds.second > node.true_objective) {
726 ++num_rc_detected_;
727 node.UpdateTrueObjective(bounds.second);
728 }
729 }
730}
731
732} // namespace sat
733} // namespace operations_research
STL vector ---------------------------------------------------------------—.
void push_back(const value_type &x)
void emplace_back(Args &&... args)
size_type size() const
void SetStopPropagationCallback(std::function< bool()> callback)
Definition integer.h:1496
An helper class to share the code used by the different kind of search.
bool GetDecision(const std::function< BooleanOrIntegerLiteral()> &f, LiteralIndex *decision)
IntegerValue LowerBound(IntegerVariable i) const
Returns the current lower/upper bound of the given integer variable.
Definition integer.h:1617
IntegerValue LevelZeroUpperBound(IntegerVariable var) const
Definition integer.h:1717
IntegerValue LevelZeroLowerBound(IntegerVariable var) const
Returns globally valid lower/upper bound on the given integer variable.
Definition integer.h:1712
std::vector< Literal > ReasonFor(IntegerLiteral literal) const
Definition integer.cc:1828
SatSolver::Status Search(const std::function< void()> &feasible_solution_observer)
Explores the search space.
A class that stores the collection of all LP constraints in a model.
const std::vector< std::unique_ptr< IntegerSumLE128 > > & OptimalConstraints() const
void BumpVariableActivities(absl::Span< const Literal > literals)
void Backtrack(int target_level)
const VariablesAssignment & Assignment() const
Definition sat_solver.h:388
const std::vector< Decision > & Decisions() const
Definition sat_solver.h:385
ABSL_MUST_USE_RESULT bool FinishPropagation()
const Trail & LiteralTrail() const
Definition sat_solver.h:387
void LogMessageWithThrottling(const std::string &prefix, const std::string &message)
void UpdateInnerObjectiveBounds(const std::string &update_info, IntegerValue lb, IntegerValue ub)
Updates the inner objective bounds.
const AssignmentInfo & Info(BooleanVariable var) const
Definition sat_base.h:441
bool LiteralIsAssigned(Literal literal) const
Definition sat_base.h:179
bool LiteralIsFalse(Literal literal) const
Definition sat_base.h:173
bool LiteralIsTrue(Literal literal) const
Definition sat_base.h:176
std::unique_ptr< SharedBoundsManager > bounds
These can be nullptr depending on the options.
int64_t value
GRBmodel * model
int literal
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
const LiteralIndex kNoLiteralIndex(-1)
std::function< BooleanOrIntegerLiteral()> IntegerValueSelectionHeuristic(std::function< BooleanOrIntegerLiteral()> var_selection_heuristic, Model *model)
std::function< BooleanOrIntegerLiteral()> SatSolverHeuristic(Model *model)
Returns the BooleanOrIntegerLiteral advised by the underlying SAT solver.
std::function< BooleanOrIntegerLiteral()> MostFractionalHeuristic(Model *model)
Choose the variable with most fractional LP value.
std::function< BooleanOrIntegerLiteral()> SequentialSearch(std::vector< std::function< BooleanOrIntegerLiteral()> > heuristics)
In SWIG mode, we don't want anything besides these top-level includes.
int64_t bound
Information about a variable assignment.
Definition sat_base.h:230
int32_t trail_index
The index of this assignment in the trail.
Definition sat_base.h:251
static IntegerLiteral GreaterOrEqual(IntegerVariable i, IntegerValue bound)
Definition integer.h:1567
std::function< BooleanOrIntegerLiteral()> fixed_search
Fixed search, built from above building blocks.