Google OR-Tools v9.12
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-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 <algorithm>
17#include <cstdint>
18#include <functional>
19#include <limits>
20#include <memory>
21#include <string>
22#include <utility>
23#include <vector>
24
25#include "absl/cleanup/cleanup.h"
26#include "absl/log/check.h"
27#include "absl/strings/str_cat.h"
28#include "absl/types/span.h"
32#include "ortools/sat/integer.h"
37#include "ortools/sat/model.h"
41#include "ortools/sat/sat_parameters.pb.h"
44#include "ortools/sat/util.h"
47
48namespace operations_research {
49namespace sat {
50
52 : name_(model->Name()),
53 time_limit_(model->GetOrCreate<TimeLimit>()),
54 random_(model->GetOrCreate<ModelRandomGenerator>()),
55 sat_solver_(model->GetOrCreate<SatSolver>()),
56 integer_encoder_(model->GetOrCreate<IntegerEncoder>()),
57 trail_(model->GetOrCreate<Trail>()),
58 assignment_(trail_->Assignment()),
59 integer_trail_(model->GetOrCreate<IntegerTrail>()),
60 watcher_(model->GetOrCreate<GenericLiteralWatcher>()),
61 shared_response_(model->GetOrCreate<SharedResponseManager>()),
62 pseudo_costs_(model->GetOrCreate<PseudoCosts>()),
63 sat_decision_(model->GetOrCreate<SatDecisionPolicy>()),
64 search_helper_(model->GetOrCreate<IntegerSearchHelper>()),
65 parameters_(*model->GetOrCreate<SatParameters>()) {
66 // We should create this class only in the presence of an objective.
67 //
68 // TODO(user): Starts with an initial variable score for all variable in
69 // the objective at their minimum value? this should emulate the first step of
70 // the core approach and gives a similar bound.
71 const ObjectiveDefinition* objective = model->Get<ObjectiveDefinition>();
72 DCHECK(objective != nullptr);
73 objective_var_ = objective->objective_var;
74
75 // Identify an LP with the same objective variable.
76 //
77 // TODO(user): if we have many independent LP, this will find nothing.
80 if (lp->ObjectiveVariable() == objective_var_) {
81 lp_constraint_ = lp;
82 }
83 }
84
85 // We use the normal SAT search but we will bump the variable activity
86 // slightly differently. In addition to the conflicts, we also bump it each
87 // time the objective lower bound increase in a sub-node.
88 std::vector<std::function<BooleanOrIntegerLiteral()>> heuristics;
89 if (SaveLpBasisOption()) {
90 heuristics.emplace_back(LpPseudoCostHeuristic(model));
91 }
92 heuristics.emplace_back(SatSolverHeuristic(model));
93 heuristics.emplace_back(MostFractionalHeuristic(model));
94 heuristics.emplace_back(IntegerValueSelectionHeuristic(
95 model->GetOrCreate<SearchHeuristics>()->fixed_search, model));
96 search_heuristic_ = SequentialSearch(std::move(heuristics));
97}
98
99bool LbTreeSearch::NodeHasBasis(const Node& node) const {
100 return !node.basis.IsEmpty();
101}
102
103bool LbTreeSearch::NodeHasUpToDateBasis(const Node& node) const {
104 if (node.basis.IsEmpty()) return false;
105
106 // TODO(user): Do something smarter. We can at least reuse the variable
107 // statuses maybe?
108 if (node.basis_timestamp != lp_constraint_->num_lp_changes()) {
109 return false;
110 }
111 return true;
112}
113
114void LbTreeSearch::EnableLpAndLoadBestBasis() {
115 DCHECK(lp_constraint_ != nullptr);
116 lp_constraint_->EnablePropagation(true);
117
118 const int level = sat_solver_->CurrentDecisionLevel();
119 if (current_branch_.empty()) return;
120
121 NodeIndex n = current_branch_[0]; // Root.
122 int basis_level = -1;
123 NodeIndex last_node_with_basis(-1);
124 for (int i = 0; i < level; ++i) {
125 if (n >= nodes_.size()) break;
126 if (NodeHasBasis(nodes_[n])) {
127 basis_level = i;
128 last_node_with_basis = n;
129 }
130 const Literal decision = sat_solver_->Decisions()[i].literal;
131 if (nodes_[n].literal_index == decision.Index()) {
132 n = nodes_[n].true_child;
133 } else {
134 DCHECK_EQ(nodes_[n].literal_index, decision.NegatedIndex());
135 n = nodes_[n].false_child;
136 }
137 }
138 if (n < nodes_.size()) {
139 if (NodeHasBasis(nodes_[n])) {
140 basis_level = level;
141 last_node_with_basis = n;
142 }
143 }
144
145 if (last_node_with_basis == -1) {
146 VLOG(1) << "no basis?";
147 return;
148 }
149 VLOG(1) << "load " << basis_level << " / " << level;
150
151 if (!NodeHasUpToDateBasis(nodes_[last_node_with_basis])) {
152 // The basis is no longer up to date, for now we do not load it.
153 // TODO(user): try to do something about it.
154 VLOG(1) << "Skipping potentially bad basis.";
155 return;
156 }
157
158 lp_constraint_->LoadBasisState(nodes_[last_node_with_basis].basis);
159}
160
161void LbTreeSearch::SaveLpBasisInto(Node& node) {
162 node.basis_timestamp = lp_constraint_->num_lp_changes();
163 node.basis = lp_constraint_->GetBasisState();
164}
165
166void LbTreeSearch::UpdateParentObjective(int level) {
167 DCHECK_GE(level, 0);
168 DCHECK_LT(level, current_branch_.size());
169 if (level == 0) return;
170 const NodeIndex parent_index = current_branch_[level - 1];
171 Node& parent = nodes_[parent_index];
172 const NodeIndex child_index = current_branch_[level];
173 const Node& child = nodes_[child_index];
174 if (parent.true_child == child_index) {
175 parent.UpdateTrueObjective(child.MinObjective());
176 } else {
177 DCHECK_EQ(parent.false_child, child_index);
178 parent.UpdateFalseObjective(child.MinObjective());
179 }
180}
181
182void LbTreeSearch::UpdateObjectiveFromParent(int level) {
183 DCHECK_GE(level, 0);
184 DCHECK_LT(level, current_branch_.size());
185 if (level == 0) return;
186 const NodeIndex parent_index = current_branch_[level - 1];
187 const Node& parent = nodes_[parent_index];
188 DCHECK_GE(parent.MinObjective(), current_objective_lb_);
189 const NodeIndex child_index = current_branch_[level];
190 Node& child = nodes_[child_index];
191 if (parent.true_child == child_index) {
192 child.UpdateObjective(parent.true_objective);
193 } else {
194 DCHECK_EQ(parent.false_child, child_index);
195 child.UpdateObjective(parent.false_objective);
196 }
197}
198
199std::string LbTreeSearch::NodeDebugString(NodeIndex n) const {
200 const IntegerValue root_lb = current_objective_lb_;
201 const auto shifted_lb = [root_lb](IntegerValue lb) {
202 return std::max<int64_t>(0, (lb - root_lb).value());
203 };
204
205 std::string s;
206 absl::StrAppend(&s, "#", n.value());
207
208 const Node& node = nodes_[n];
209 std::string true_letter = "t";
210 std::string false_letter = "f";
211 if (node.literal_index != kNoLiteralIndex && !node.is_deleted) {
212 const Literal decision = node.Decision();
213 if (assignment_.LiteralIsTrue(decision)) {
214 true_letter = "T";
215 }
216 if (assignment_.LiteralIsFalse(decision)) {
217 false_letter = "F";
218 }
219 }
220
221 if (node.true_child < nodes_.size()) {
222 absl::StrAppend(&s, " [", true_letter, ":#", node.true_child.value(), " ",
223 shifted_lb(node.true_objective), "]");
224 } else {
225 absl::StrAppend(&s, " [", true_letter, ":## ",
226 shifted_lb(node.true_objective), "]");
227 }
228 if (node.false_child < nodes_.size()) {
229 absl::StrAppend(&s, " [", false_letter, ":#", node.false_child.value(), " ",
230 shifted_lb(node.false_objective), "]");
231 } else {
232 absl::StrAppend(&s, " [", false_letter, ":## ",
233 shifted_lb(node.false_objective), "]");
234 }
235
236 if (node.is_deleted) {
237 absl::StrAppend(&s, " <D>");
238 }
239 if (NodeHasBasis(node)) {
240 absl::StrAppend(&s, " <B>");
241 }
242
243 return s;
244}
245
246void LbTreeSearch::DebugDisplayTree(NodeIndex root) const {
247 int num_nodes = 0;
248
249 util_intops::StrongVector<NodeIndex, int> level(nodes_.size(), 0);
250 std::vector<NodeIndex> to_explore = {root};
251 while (!to_explore.empty()) {
252 NodeIndex n = to_explore.back();
253 to_explore.pop_back();
254
255 ++num_nodes;
256 const Node& node = nodes_[n];
257
258 std::string s(level[n], ' ');
259 absl::StrAppend(&s, NodeDebugString(n));
260 LOG(INFO) << s;
261
262 if (node.true_child < nodes_.size()) {
263 to_explore.push_back(node.true_child);
264 level[node.true_child] = level[n] + 1;
265 }
266 if (node.false_child < nodes_.size()) {
267 to_explore.push_back(node.false_child);
268 level[node.false_child] = level[n] + 1;
269 }
270 }
271 LOG(INFO) << "num_nodes: " << num_nodes;
272}
273
274// Here we forgot the whole search tree and restart.
275//
276// The idea is that the heuristic has now more information so it will likely
277// take better decision which will result in a smaller overall tree.
278bool LbTreeSearch::FullRestart() {
279 ++num_full_restarts_;
280 num_decisions_taken_at_last_restart_ = num_decisions_taken_;
281 num_nodes_in_tree_ = 0;
282 nodes_.clear();
283 current_branch_.clear();
284 return sat_solver_->RestoreSolverToAssumptionLevel();
285}
286
287void LbTreeSearch::MarkAsDeletedNodeAndUnreachableSubtree(Node& node) {
288 --num_nodes_in_tree_;
289 DCHECK(!node.is_deleted);
290 node.is_deleted = true;
291 DCHECK_NE(node.literal_index, kNoLiteralIndex);
292 if (assignment_.LiteralIsTrue(Literal(node.literal_index))) {
293 MarkSubtreeAsDeleted(node.false_child);
294 } else {
295 MarkSubtreeAsDeleted(node.true_child);
296 }
297}
298
299void LbTreeSearch::MarkBranchAsInfeasible(Node& node, bool true_branch) {
300 if (true_branch) {
301 node.UpdateTrueObjective(kMaxIntegerValue);
302 MarkSubtreeAsDeleted(node.true_child);
303 node.true_child = NodeIndex(std::numeric_limits<int32_t>::max());
304 } else {
305 node.UpdateFalseObjective(kMaxIntegerValue);
306 MarkSubtreeAsDeleted(node.false_child);
307 node.false_child = NodeIndex(std::numeric_limits<int32_t>::max());
308 }
309}
310
311void LbTreeSearch::MarkSubtreeAsDeleted(NodeIndex root) {
312 std::vector<NodeIndex> to_delete{root};
313 for (int i = 0; i < to_delete.size(); ++i) {
314 const NodeIndex n = to_delete[i];
315 if (n >= nodes_.size()) continue;
316 if (nodes_[n].is_deleted) continue;
317
318 --num_nodes_in_tree_;
319 nodes_[n].is_deleted = true;
320
321 to_delete.push_back(nodes_[n].true_child);
322 to_delete.push_back(nodes_[n].false_child);
323 }
324}
325
326std::string LbTreeSearch::SmallProgressString() const {
327 return absl::StrCat(
328 "nodes=", num_nodes_in_tree_, "/", nodes_.size(),
329 " rc=", num_rc_detected_, " decisions=", num_decisions_taken_,
330 " @root=", num_back_to_root_node_, " restarts=", num_full_restarts_,
331 " lp_iters=[", FormatCounter(num_lp_iters_at_level_zero_), ", ",
332 FormatCounter(num_lp_iters_save_basis_), ", ",
333 FormatCounter(num_lp_iters_first_branch_), ", ",
334 FormatCounter(num_lp_iters_dive_), "]");
335}
336
337std::function<void()> LbTreeSearch::UpdateLpIters(int64_t* counter) {
338 if (lp_constraint_ == nullptr) return []() {};
339 const int64_t old_num = lp_constraint_->total_num_simplex_iterations();
340 return [old_num, counter, this]() {
341 const int64_t new_num = lp_constraint_->total_num_simplex_iterations();
342 *counter += new_num - old_num;
343 };
344}
345
346bool LbTreeSearch::LevelZeroLogic() {
347 ++num_back_to_root_node_;
348 num_decisions_taken_at_last_level_zero_ = num_decisions_taken_;
349
350 // Always run the LP when we are back at level zero.
351 if (SaveLpBasisOption() && !current_branch_.empty()) {
352 const auto cleanup =
353 absl::MakeCleanup(UpdateLpIters(&num_lp_iters_at_level_zero_));
354 EnableLpAndLoadBestBasis();
355 if (!sat_solver_->FinishPropagation()) {
356 return false;
357 }
358 SaveLpBasisInto(nodes_[current_branch_[0]]);
359 lp_constraint_->EnablePropagation(false);
360 }
361
362 // Import the objective upper-bound.
363 // We do that manually because we disabled objective import to not "pollute"
364 // the objective lower_bound and still have local reason for objective
365 // improvement.
366 {
367 const IntegerValue ub = shared_response_->GetInnerObjectiveUpperBound();
368 if (integer_trail_->UpperBound(objective_var_) > ub) {
369 if (!integer_trail_->Enqueue(
370 IntegerLiteral::LowerOrEqual(objective_var_, ub), {}, {})) {
371 sat_solver_->NotifyThatModelIsUnsat();
372 return false;
373 }
374 if (!sat_solver_->FinishPropagation()) {
375 return false;
376 }
377 }
378 }
379
380 // If the search has not just been restarted (in which case nodes_ would be
381 // empty), and if we are at level zero (either naturally, or if the
382 // backtrack level was set to zero in the above code), let's run a different
383 // heuristic to decide whether to restart the search from scratch or not.
384 //
385 // We ignore small search trees.
386 if (num_nodes_in_tree_ > 50) {
387 // Let's count how many nodes have worse objective bounds than the best
388 // known external objective lower bound.
389 const IntegerValue latest_lb =
390 shared_response_->GetInnerObjectiveLowerBound();
391 int num_nodes = 0;
392 int num_nodes_with_lower_objective = 0;
393 for (const Node& node : nodes_) {
394 if (node.is_deleted) continue;
395 ++num_nodes;
396 if (node.MinObjective() < latest_lb) num_nodes_with_lower_objective++;
397 }
398 DCHECK_EQ(num_nodes_in_tree_, num_nodes);
399 if (num_nodes_with_lower_objective * 2 > num_nodes) {
400 VLOG(2) << "lb_tree_search restart nodes: "
401 << num_nodes_with_lower_objective << "/" << num_nodes << " : "
402 << 100.0 * num_nodes_with_lower_objective / num_nodes << "%"
403 << ", decisions:" << num_decisions_taken_;
404 if (!FullRestart()) return false;
405 }
406 }
407
408 return true;
409}
410
412 const std::function<void()>& feasible_solution_observer) {
413 if (!sat_solver_->RestoreSolverToAssumptionLevel()) {
414 return sat_solver_->UnsatStatus();
415 }
416
417 // We currently restart the search tree from scratch from time to times:
418 // - Initially, every kNumDecisionsBeforeInitialRestarts, for at most
419 // kMaxNumInitialRestarts times.
420 // - Every time we backtrack to level zero, we count how many nodes are worse
421 // than the best known objective lower bound. If this is true for more than
422 // half of the existing nodes, we restart and clear all nodes. If if this
423 // happens during the initial restarts phase, it reset the above counter and
424 // uses 1 of the available initial restarts.
425 //
426 // This has 2 advantages:
427 // - It allows our "pseudo-cost" to kick in and experimentally result in
428 // smaller trees down the road.
429 // - It removes large inefficient search trees.
430 //
431 // TODO(user): a strong branching initial start, or allowing a few decision
432 // per nodes might be a better approach.
433 //
434 // TODO(user): It would also be cool to exploit the reason for the LB increase
435 // even more.
436 const int kMaxNumInitialRestarts = 10;
437 const int64_t kNumDecisionsBeforeInitialRestarts = 1000;
438
439 // If some branches already have a good lower bound, no need to call the LP
440 // on those.
441 watcher_->SetStopPropagationCallback([this] {
442 return integer_trail_->LowerBound(objective_var_) > current_objective_lb_;
443 });
444
445 while (!time_limit_->LimitReached() && !shared_response_->ProblemIsSolved()) {
446 VLOG(2) << "LOOP " << sat_solver_->CurrentDecisionLevel();
447
448 // Each time we are back here, we bump the activities of the variable that
449 // are part of the objective lower bound reason.
450 //
451 // Note that this is why we prefer not to increase the lower zero lower
452 // bound of objective_var_ with the tree root lower bound, so we can exploit
453 // more reasons.
454 //
455 // TODO(user): This is slightly different than bumping each time we
456 // push a decision that result in an LB increase. This is also called on
457 // backjump for instance.
458 const IntegerValue obj_diff =
459 integer_trail_->LowerBound(objective_var_) -
460 integer_trail_->LevelZeroLowerBound(objective_var_);
461 if (obj_diff > 0) {
462 std::vector<Literal> reason =
463 integer_trail_->ReasonFor(IntegerLiteral::GreaterOrEqual(
464 objective_var_, integer_trail_->LowerBound(objective_var_)));
465
466 // TODO(user): We also need to update pseudo cost on conflict.
467 pseudo_costs_->UpdateBoolPseudoCosts(reason, obj_diff);
468 sat_decision_->BumpVariableActivities(reason);
469 sat_decision_->UpdateVariableActivityIncrement();
470 }
471
472 // Propagate upward in the tree the new objective lb.
473 if (!current_branch_.empty()) {
474 // Our branch is always greater or equal to the level.
475 // We increase the objective_lb of the current node if needed.
476 {
477 const int current_level = sat_solver_->CurrentDecisionLevel();
478 const IntegerValue current_objective_lb =
479 integer_trail_->LowerBound(objective_var_);
480 if (DEBUG_MODE) {
481 CHECK_LE(current_level, current_branch_.size());
482 for (int i = 0; i < current_level; ++i) {
483 CHECK(!nodes_[current_branch_[i]].is_deleted);
484 CHECK(assignment_.LiteralIsAssigned(
485 nodes_[current_branch_[i]].Decision()));
486 }
487 }
488 if (current_level < current_branch_.size()) {
489 nodes_[current_branch_[current_level]].UpdateObjective(
490 current_objective_lb);
491 }
492
493 // Minor optim: sometimes, because of the LP and cuts, the reason for
494 // objective_var_ only contains lower level literals, so we can exploit
495 // that.
496 //
497 // TODO(user): No point checking that if the objective lb wasn't
498 // assigned at this level.
499 //
500 // TODO(user): Exploit the reasons further.
501 if (current_objective_lb >
502 integer_trail_->LevelZeroLowerBound(objective_var_)) {
503 const std::vector<Literal> reason =
504 integer_trail_->ReasonFor(IntegerLiteral::GreaterOrEqual(
505 objective_var_, current_objective_lb));
506 int max_level = 0;
507 for (const Literal l : reason) {
508 max_level = std::max<int>(
509 max_level,
510 sat_solver_->LiteralTrail().Info(l.Variable()).level);
511 }
512 if (max_level < current_level) {
513 nodes_[current_branch_[max_level]].UpdateObjective(
514 current_objective_lb);
515 }
516 }
517 }
518
519 // Propagate upward any new bounds.
520 for (int level = current_branch_.size(); --level > 0;) {
521 UpdateParentObjective(level);
522 }
523 }
524
525 if (SaveLpBasisOption()) {
526 // We disable LP automatic propagation and only enable it:
527 // - at root node
528 // - when we go to a new branch.
529 lp_constraint_->EnablePropagation(false);
530 }
531 if (sat_solver_->ModelIsUnsat()) return sat_solver_->UnsatStatus();
532
533 // This will import other workers bound if we are back to level zero.
534 // It might also decide to restart.
535 if (!search_helper_->BeforeTakingDecision()) {
536 return sat_solver_->UnsatStatus();
537 }
538
539 // This is the current bound we try to improve. We cache it here to avoid
540 // getting the lock many times and it is also easier to follow the code if
541 // this is assumed constant for one iteration.
542 current_objective_lb_ = shared_response_->GetInnerObjectiveLowerBound();
543 if (!current_branch_.empty()) {
544 nodes_[current_branch_[0]].UpdateObjective(current_objective_lb_);
545 for (int i = 1; i < current_branch_.size(); ++i) {
546 UpdateObjectiveFromParent(i);
547 }
548
549 // If the root lb increased, update global shared objective lb.
550 const IntegerValue bound = nodes_[current_branch_[0]].MinObjective();
551 if (bound > current_objective_lb_) {
552 shared_response_->UpdateInnerObjectiveBounds(
553 absl::StrCat(name_, " (", SmallProgressString(), ") "), bound,
554 integer_trail_->LevelZeroUpperBound(objective_var_));
555 current_objective_lb_ = bound;
556 if (VLOG_IS_ON(3)) DebugDisplayTree(current_branch_[0]);
557 }
558 }
559
560 // Forget the whole tree and restart.
561 // We will do it periodically at the beginning of the search each time we
562 // cross the kNumDecisionsBeforeInitialRestarts decision since the last
563 // restart. This will happen at most kMaxNumInitialRestarts times.
564 if (num_decisions_taken_ >= num_decisions_taken_at_last_restart_ +
565 kNumDecisionsBeforeInitialRestarts &&
566 num_full_restarts_ < kMaxNumInitialRestarts) {
567 VLOG(2) << "lb_tree_search (initial_restart " << SmallProgressString()
568 << ")";
569 if (!FullRestart()) return sat_solver_->UnsatStatus();
570 continue;
571 }
572
573 // Periodic backtrack to level zero so we can import bounds.
574 if (num_decisions_taken_ >=
575 num_decisions_taken_at_last_level_zero_ + 10000) {
576 if (!sat_solver_->ResetToLevelZero()) {
577 return sat_solver_->UnsatStatus();
578 }
579 }
580
581 // Backtrack if needed.
582 //
583 // Our algorithm stop exploring a branch as soon as its objective lower
584 // bound is greater than the root lower bound. We then backtrack to the
585 // first node in the branch that is not yet closed under this bound.
586 //
587 // TODO(user): If we remember how far we can backjump for both true/false
588 // branch, we could be more efficient.
589 while (current_branch_.size() > sat_solver_->CurrentDecisionLevel() + 1 ||
590 (current_branch_.size() > 1 &&
591 nodes_[current_branch_.back()].MinObjective() >
592 current_objective_lb_)) {
593 current_branch_.pop_back();
594 }
595
596 // Backtrack the solver to be in sync with current_branch_.
597 {
598 const int backtrack_level =
599 std::max(0, static_cast<int>(current_branch_.size()) - 1);
600 sat_solver_->Backtrack(backtrack_level);
601 if (!sat_solver_->FinishPropagation()) {
602 return sat_solver_->UnsatStatus();
603 }
604 if (sat_solver_->CurrentDecisionLevel() < backtrack_level) continue;
605 }
606
607 if (sat_solver_->CurrentDecisionLevel() == 0) {
608 if (!LevelZeroLogic()) {
609 return sat_solver_->UnsatStatus();
610 }
611 }
612
613 // Dive: Follow the branch with lowest objective.
614 // Note that we do not creates new nodes here.
615 //
616 // TODO(user): If we have new information and our current objective bound
617 // is higher than any bound in a whole subtree, we might want to just
618 // restart this subtree exploration?
619 while (true) {
620 const int size = current_branch_.size();
621 const int level = sat_solver_->CurrentDecisionLevel();
622
623 // Invariant are tricky:
624 // current_branch_ contains one entry per decision taken + the last one
625 // which we are about to take. If we don't have the last entry, it means
626 // we are about to take a new decision.
627 DCHECK(size == level || size == level + 1);
628 if (size == level) break; // Take new decision.
629
630 const NodeIndex node_index = current_branch_[level];
631 Node& node = nodes_[node_index];
632 DCHECK_GT(node.true_child, node_index);
633 DCHECK_GT(node.false_child, node_index);
634
635 // If the bound of this node is high, restart the main loop..
636 node.UpdateObjective(std::max(
637 current_objective_lb_, integer_trail_->LowerBound(objective_var_)));
638 if (node.MinObjective() > current_objective_lb_) break;
639 DCHECK_EQ(node.MinObjective(), current_objective_lb_) << level;
640
641 // This will be set to the next node index.
642 NodeIndex n;
643 DCHECK(!node.is_deleted);
644 const Literal node_literal = node.Decision();
645
646 // If the variable is already fixed, we bypass the node and connect
647 // its parent directly to the relevant child.
648 if (assignment_.LiteralIsAssigned(node_literal)) {
649 IntegerValue new_lb;
650 if (assignment_.LiteralIsTrue(node_literal)) {
651 n = node.true_child;
652 new_lb = node.true_objective;
653 } else {
654 n = node.false_child;
655 new_lb = node.false_objective;
656 }
657 MarkAsDeletedNodeAndUnreachableSubtree(node);
658
659 // We jump directly to the subnode.
660 // Else we will change the root.
661 current_branch_.pop_back();
662 if (!current_branch_.empty()) {
663 const NodeIndex parent = current_branch_.back();
664 DCHECK(!nodes_[parent].is_deleted);
665 const Literal parent_literal = nodes_[parent].Decision();
666 if (assignment_.LiteralIsTrue(parent_literal)) {
667 nodes_[parent].true_child = n;
668 nodes_[parent].UpdateTrueObjective(new_lb);
669 } else {
670 DCHECK(assignment_.LiteralIsFalse(parent_literal));
671 nodes_[parent].false_child = n;
672 nodes_[parent].UpdateFalseObjective(new_lb);
673 }
674 if (new_lb > current_objective_lb_) {
675 // This is probably not needed.
676 if (n < nodes_.size() && !nodes_[n].IsLeaf()) {
677 current_branch_.push_back(n);
678 nodes_[n].UpdateObjective(new_lb);
679 }
680 break;
681 }
682 } else {
683 if (n >= nodes_.size()) {
684 // We never explored the other branch, so we can just clear all
685 // nodes.
686 num_nodes_in_tree_ = 0;
687 nodes_.clear();
688 } else if (nodes_[n].IsLeaf()) {
689 // Keep the saved basis.
690 num_nodes_in_tree_ = 1;
691 Node root = std::move(nodes_[n]);
692 nodes_.clear();
693 nodes_.push_back(std::move(root));
694 n = NodeIndex(0);
695 } else {
696 // We always make sure the root is at zero.
697 // The root is no longer at zero, that might cause issue.
698 // Cleanup.
699 }
700 }
701 } else {
702 // See if we have better bounds using the current LP state.
703 ExploitReducedCosts(current_branch_[level]);
704 if (node.MinObjective() > current_objective_lb_) break;
705
706 // If both lower bound are the same, we pick the literal branch. We do
707 // that because this is the polarity that was chosen by the SAT
708 // heuristic in the first place. We tried random, it doesn't seems to
709 // work as well.
710 num_decisions_taken_++;
711 const bool choose_true = node.true_objective <= node.false_objective;
712 Literal next_decision;
713 if (choose_true) {
714 n = node.true_child;
715 next_decision = node_literal;
716 } else {
717 n = node.false_child;
718 next_decision = node_literal.Negated();
719 }
720
721 // If we are taking this branch for the first time, we enable the LP and
722 // make sure we solve it before taking the decision. This allow to have
723 // proper pseudo-costs, and also be incremental for the decision we are
724 // about to take.
725 //
726 // We also enable the LP if we have no basis info for this node.
727 if (SaveLpBasisOption() &&
728 (n >= nodes_.size() || !NodeHasBasis(node))) {
729 const auto cleanup =
730 absl::MakeCleanup(UpdateLpIters(&num_lp_iters_save_basis_));
731
732 VLOG(1) << "~~~~";
733 EnableLpAndLoadBestBasis();
734 const int level = sat_solver_->CurrentDecisionLevel();
735 if (!sat_solver_->FinishPropagation()) {
736 return sat_solver_->UnsatStatus();
737 }
738 if (sat_solver_->CurrentDecisionLevel() < level) {
739 node.UpdateObjective(kMaxIntegerValue);
740 break;
741 }
742
743 // The decision might have become assigned, in which case we loop.
744 if (assignment_.LiteralIsAssigned(next_decision)) {
745 continue;
746 }
747
748 SaveLpBasisInto(node);
749
750 // If we are not at the end, disable the LP propagation.
751 if (n < nodes_.size()) {
752 lp_constraint_->EnablePropagation(false);
753 }
754 }
755
756 // Take the decision.
757 const auto cleanup =
758 absl::MakeCleanup(UpdateLpIters(&num_lp_iters_first_branch_));
759 DCHECK(!assignment_.LiteralIsAssigned(next_decision));
760 search_helper_->TakeDecision(next_decision);
761
762 // Conflict?
763 if (current_branch_.size() != sat_solver_->CurrentDecisionLevel()) {
764 MarkBranchAsInfeasible(node, choose_true);
765 break;
766 }
767
768 // Update the proper field and abort the dive if we crossed the
769 // threshold.
770 const IntegerValue lb = integer_trail_->LowerBound(objective_var_);
771 if (choose_true) {
772 node.UpdateTrueObjective(lb);
773 } else {
774 node.UpdateFalseObjective(lb);
775 }
776
777 if (n < nodes_.size()) {
778 nodes_[n].UpdateObjective(lb);
779 } else if (SaveLpBasisOption()) {
780 SaveLpBasisInto(nodes_[CreateNewEmptyNodeIfNeeded()]);
781 }
782
783 if (lb > current_objective_lb_) break;
784 }
785
786 if (VLOG_IS_ON(1)) {
787 shared_response_->LogMessageWithThrottling(
788 "TreeS", absl::StrCat(" (", SmallProgressString(), ")"));
789 }
790
791 if (n < nodes_.size() && !nodes_[n].IsLeaf()) {
792 current_branch_.push_back(n);
793 } else {
794 break;
795 }
796 }
797
798 // If a conflict occurred, we will backtrack.
799 if (current_branch_.size() != sat_solver_->CurrentDecisionLevel()) {
800 continue;
801 }
802
803 // TODO(user): The code is hard to follow. Fix and merge that with test
804 // below.
805 if (!current_branch_.empty()) {
806 const Node& final_node = nodes_[current_branch_.back()];
807 if (assignment_.LiteralIsTrue(final_node.Decision())) {
808 if (final_node.true_objective > current_objective_lb_) {
809 continue;
810 }
811 } else {
812 DCHECK(assignment_.LiteralIsFalse(final_node.Decision()));
813 if (final_node.false_objective > current_objective_lb_) {
814 continue;
815 }
816 }
817 }
818
819 // This test allow to not take a decision when the branch is already closed
820 // (i.e. the true branch or false branch lb is high enough). Adding it
821 // basically changes if we take the decision later when we explore the
822 // branch or right now.
823 //
824 // I feel taking it later is better. It also avoid creating unneeded nodes.
825 // It does change the behavior on a few problem though. For instance on
826 // irp.mps.gz, the search works better without this, whatever the random
827 // seed. Not sure why, maybe it creates more diversity?
828 //
829 // Another difference is that if the search is done and we have a feasible
830 // solution, we will not report it because of this test (except if we are
831 // at the optimal).
832 if (integer_trail_->LowerBound(objective_var_) > current_objective_lb_) {
833 continue;
834 }
835
836 const auto cleanup = absl::MakeCleanup(UpdateLpIters(&num_lp_iters_dive_));
837
838 if (current_branch_.empty()) {
839 VLOG(2) << "DIVE from empty tree";
840 } else {
841 VLOG(2) << "DIVE from " << NodeDebugString(current_branch_.back());
842 }
843
844 if (SaveLpBasisOption() && !lp_constraint_->PropagationIsEnabled()) {
845 // This reuse or create a node to store the basis.
846 const NodeIndex index = CreateNewEmptyNodeIfNeeded();
847
848 EnableLpAndLoadBestBasis();
849 const int level = sat_solver_->CurrentDecisionLevel();
850 if (!sat_solver_->FinishPropagation()) {
851 return sat_solver_->UnsatStatus();
852 }
853
854 // Loop on backtrack or bound improvement.
855 if (sat_solver_->CurrentDecisionLevel() < level) {
856 Node& node = nodes_[index];
857 node.UpdateObjective(kMaxIntegerValue);
858 continue;
859 }
860
861 SaveLpBasisInto(nodes_[index]);
862
863 const IntegerValue obj_lb = integer_trail_->LowerBound(objective_var_);
864 if (obj_lb > current_objective_lb_) {
865 nodes_[index].UpdateObjective(obj_lb);
866 if (!current_branch_.empty()) {
867 Node& parent_node = nodes_[current_branch_.back()];
868 const Literal node_literal = parent_node.Decision();
869 DCHECK(assignment_.LiteralIsAssigned(node_literal));
870 if (assignment_.LiteralIsTrue(node_literal)) {
871 parent_node.UpdateTrueObjective(obj_lb);
872 } else {
873 parent_node.UpdateFalseObjective(obj_lb);
874 }
875 }
876 continue;
877 }
878 }
879
880 // Invariant: The current branch is fully assigned, and the solver is in
881 // sync. And we are not on a "bad" path.
882 const int base_level = sat_solver_->CurrentDecisionLevel();
883 if (DEBUG_MODE) {
884 CHECK_EQ(base_level, current_branch_.size());
885 for (const NodeIndex index : current_branch_) {
886 CHECK(!nodes_[index].is_deleted);
887 const Literal decision = nodes_[index].Decision();
888 if (assignment_.LiteralIsTrue(decision)) {
889 CHECK_EQ(nodes_[index].true_objective, current_objective_lb_);
890 } else {
891 CHECK(assignment_.LiteralIsFalse(decision));
892 CHECK_EQ(nodes_[index].false_objective, current_objective_lb_);
893 }
894 }
895 }
896
897 // We are about to take a new decision, what we will do is dive until
898 // the objective lower bound increase. we will then create a bunch of new
899 // nodes in the tree.
900 //
901 // By analyzing the reason for the increase, we can create less nodes than
902 // if we just followed the initial heuristic.
903 //
904 // TODO(user): In multithread, this change the behavior a lot since we
905 // dive until we beat the best shared bound. Maybe we shouldn't do that.
906 while (true) {
907 // TODO(user): We sometimes branch on the objective variable, this should
908 // probably be avoided.
909 if (sat_solver_->ModelIsUnsat()) return sat_solver_->UnsatStatus();
910 LiteralIndex decision;
911 if (!search_helper_->GetDecision(search_heuristic_, &decision)) {
912 continue;
913 }
914
915 // No new decision: search done.
916 if (time_limit_->LimitReached()) return SatSolver::LIMIT_REACHED;
917 if (decision == kNoLiteralIndex) {
918 feasible_solution_observer();
919 break;
920 }
921
922 num_decisions_taken_++;
923 if (!search_helper_->TakeDecision(Literal(decision))) {
924 return sat_solver_->UnsatStatus();
925 }
926 if (sat_solver_->CurrentDecisionLevel() < base_level) {
927 // TODO(user): it would be nice to mark some node as infeasible if
928 // this is the case. However this could happen after many decision and
929 // we realize with the lp that one of them should have been fixed
930 // earlier, without any infeasibility in the current branch.
931 break;
932 }
933 if (integer_trail_->LowerBound(objective_var_) > current_objective_lb_) {
934 break;
935 }
936 }
937
938 if (sat_solver_->CurrentDecisionLevel() <= base_level) continue;
939
940 // Analyse the reason for objective increase. Deduce a set of new nodes to
941 // append to the tree.
942 //
943 // TODO(user): Try to minimize the number of decisions?
944 const std::vector<Literal> reason =
945 integer_trail_->ReasonFor(IntegerLiteral::GreaterOrEqual(
946 objective_var_, integer_trail_->LowerBound(objective_var_)));
947 std::vector<Literal> decisions = ExtractDecisions(base_level, reason);
948
949 // Bump activities.
950 sat_decision_->BumpVariableActivities(reason);
951 sat_decision_->BumpVariableActivities(decisions);
952 sat_decision_->UpdateVariableActivityIncrement();
953
954 // Create one node per new decisions.
955 DCHECK_EQ(current_branch_.size(), base_level);
956 for (const Literal d : decisions) {
957 AppendNewNodeToCurrentBranch(d);
958 }
959
960 // TODO(user): We should probably save the basis in more cases.
961 if (SaveLpBasisOption() && decisions.size() == 1) {
962 SaveLpBasisInto(nodes_[CreateNewEmptyNodeIfNeeded()]);
963 }
964
965 // Update the objective of the last node in the branch since we just
966 // improved that.
967 if (!current_branch_.empty()) {
968 Node& n = nodes_[current_branch_.back()];
969 if (assignment_.LiteralIsTrue(n.Decision())) {
970 n.UpdateTrueObjective(integer_trail_->LowerBound(objective_var_));
971 } else {
972 n.UpdateFalseObjective(integer_trail_->LowerBound(objective_var_));
973 }
974 }
975
976 // Reset the solver to a correct state since we have a subset of the
977 // current propagation. We backtrack as little as possible.
978 //
979 // The decision level is the number of decision taken.
980 // Decision()[level] is the decision at that level.
981 int backtrack_level = base_level;
982 DCHECK_LE(current_branch_.size(), sat_solver_->CurrentDecisionLevel());
983 while (backtrack_level < current_branch_.size() &&
984 sat_solver_->Decisions()[backtrack_level].literal.Index() ==
985 nodes_[current_branch_[backtrack_level]].literal_index) {
986 ++backtrack_level;
987 }
988 sat_solver_->Backtrack(backtrack_level);
989
990 // Update bounds with reduced costs info.
991 //
992 // TODO(user): Uses old optimal constraint that we just potentially
993 // backtracked over?
994 //
995 // TODO(user): We could do all at once rather than in O(#decision * #size).
996 for (int i = backtrack_level; i < current_branch_.size(); ++i) {
997 ExploitReducedCosts(current_branch_[i]);
998 }
999 }
1000
1002}
1003
1004std::vector<Literal> LbTreeSearch::ExtractDecisions(
1005 int base_level, absl::Span<const Literal> conflict) {
1006 std::vector<int> num_per_level(sat_solver_->CurrentDecisionLevel() + 1, 0);
1007 std::vector<bool> is_marked;
1008 for (const Literal l : conflict) {
1009 const AssignmentInfo& info = trail_->Info(l.Variable());
1010 if (info.level <= base_level) continue;
1011 num_per_level[info.level]++;
1012 if (info.trail_index >= is_marked.size()) {
1013 is_marked.resize(info.trail_index + 1);
1014 }
1015 is_marked[info.trail_index] = true;
1016 }
1017
1018 std::vector<Literal> result;
1019 if (is_marked.empty()) return result;
1020 for (int i = is_marked.size() - 1; i >= 0; --i) {
1021 if (!is_marked[i]) continue;
1022
1023 const Literal l = (*trail_)[i];
1024 const AssignmentInfo& info = trail_->Info(l.Variable());
1025 if (info.level <= base_level) break;
1026 if (num_per_level[info.level] == 1) {
1027 result.push_back(l);
1028 continue;
1029 }
1030
1031 // Expand.
1032 num_per_level[info.level]--;
1033 for (const Literal new_l : trail_->Reason(l.Variable())) {
1034 const AssignmentInfo& new_info = trail_->Info(new_l.Variable());
1035 if (new_info.level <= base_level) continue;
1036 if (is_marked[new_info.trail_index]) continue;
1037 is_marked[new_info.trail_index] = true;
1038 num_per_level[new_info.level]++;
1039 }
1040 }
1041
1042 // We prefer to keep the same order.
1043 std::reverse(result.begin(), result.end());
1044 return result;
1045}
1046
1047LbTreeSearch::NodeIndex LbTreeSearch::CreateNewEmptyNodeIfNeeded() {
1048 NodeIndex n(0);
1049 if (current_branch_.empty()) {
1050 if (nodes_.empty()) {
1051 ++num_nodes_in_tree_;
1052 nodes_.emplace_back(current_objective_lb_);
1053 } else {
1054 DCHECK_EQ(nodes_.size(), 1);
1055 }
1056 } else {
1057 const NodeIndex parent = current_branch_.back();
1058 DCHECK(!nodes_[parent].is_deleted);
1059 const Literal parent_literal = nodes_[parent].Decision();
1060 if (assignment_.LiteralIsTrue(parent_literal)) {
1061 if (nodes_[parent].true_child >= nodes_.size()) {
1062 n = NodeIndex(nodes_.size());
1063 ++num_nodes_in_tree_;
1064 nodes_[parent].true_child = NodeIndex(nodes_.size());
1065 nodes_.emplace_back(current_objective_lb_);
1066 } else {
1067 n = nodes_[parent].true_child;
1068 }
1069 nodes_[parent].UpdateTrueObjective(current_objective_lb_);
1070 } else {
1071 DCHECK(assignment_.LiteralIsFalse(parent_literal));
1072 if (nodes_[parent].false_child >= nodes_.size()) {
1073 n = NodeIndex(nodes_.size());
1074 ++num_nodes_in_tree_;
1075 nodes_[parent].false_child = NodeIndex(nodes_.size());
1076 nodes_.emplace_back(current_objective_lb_);
1077 } else {
1078 n = nodes_[parent].false_child;
1079 }
1080 nodes_[parent].UpdateFalseObjective(current_objective_lb_);
1081 }
1082 }
1083 DCHECK(!nodes_[n].is_deleted);
1084 DCHECK_EQ(nodes_[n].literal_index, kNoLiteralIndex);
1085 return n;
1086}
1087
1088void LbTreeSearch::AppendNewNodeToCurrentBranch(Literal decision) {
1089 NodeIndex n(nodes_.size());
1090 if (current_branch_.empty()) {
1091 if (nodes_.empty()) {
1092 ++num_nodes_in_tree_;
1093 nodes_.emplace_back(current_objective_lb_);
1094 } else {
1095 DCHECK_EQ(nodes_.size(), 1);
1096 n = 0;
1097 }
1098 } else {
1099 const NodeIndex parent = current_branch_.back();
1100 DCHECK(!nodes_[parent].is_deleted);
1101 const Literal parent_literal = nodes_[parent].Decision();
1102 if (assignment_.LiteralIsTrue(parent_literal)) {
1103 if (nodes_[parent].true_child < nodes_.size()) {
1104 n = nodes_[parent].true_child;
1105 } else {
1106 ++num_nodes_in_tree_;
1107 nodes_.emplace_back(current_objective_lb_);
1108 nodes_[parent].true_child = n;
1109 }
1110 nodes_[parent].UpdateTrueObjective(current_objective_lb_);
1111 } else {
1112 DCHECK(assignment_.LiteralIsFalse(parent_literal));
1113 if (nodes_[parent].false_child < nodes_.size()) {
1114 n = nodes_[parent].false_child;
1115 } else {
1116 ++num_nodes_in_tree_;
1117 nodes_.emplace_back(current_objective_lb_);
1118 nodes_[parent].false_child = n;
1119 }
1120 nodes_[parent].UpdateFalseObjective(current_objective_lb_);
1121 }
1122 }
1123 DCHECK_LT(n, nodes_.size());
1124 DCHECK_EQ(nodes_[n].literal_index, kNoLiteralIndex) << " issue " << n;
1125 nodes_[n].SetDecision(decision);
1126 nodes_[n].UpdateObjective(current_objective_lb_);
1127 current_branch_.push_back(n);
1128}
1129
1130// Looking at the reduced costs, we can already have a bound for one of the
1131// branch. Increasing the corresponding objective can save some branches,
1132// and also allow for a more incremental LP solving since we do less back
1133// and forth.
1134//
1135// TODO(user): The code to recover that is a bit convoluted. Alternatively
1136// Maybe we should do a "fast" propagation without the LP in each branch.
1137// That will work as long as we keep these optimal LP constraints around
1138// and propagate them.
1139//
1140// TODO(user): Incorporate this in the heuristic so we choose more Booleans
1141// inside these LP explanations?
1142void LbTreeSearch::ExploitReducedCosts(NodeIndex n) {
1143 if (lp_constraint_ == nullptr) return;
1144
1145 // TODO(user): we could consider earlier constraints instead of just
1146 // looking at the last one, but experiments didn't really show a big
1147 // gain.
1148 const auto& cts = lp_constraint_->OptimalConstraints();
1149 if (cts.empty()) return;
1150 const std::unique_ptr<IntegerSumLE128>& rc = cts.back();
1151
1152 // Note that this return literal EQUIVALENT to the node.literal, not just
1153 // implied by it. We need that for correctness.
1154 int num_tests = 0;
1155 Node& node = nodes_[n];
1156 DCHECK(!node.is_deleted);
1157 const Literal node_literal = node.Decision();
1158 DCHECK(!assignment_.LiteralIsAssigned(node_literal));
1159 for (const IntegerLiteral integer_literal :
1160 integer_encoder_->GetIntegerLiterals(node_literal)) {
1161 // To avoid bad corner case. Not sure it ever triggers.
1162 if (++num_tests > 10) break;
1163
1164 const std::pair<IntegerValue, IntegerValue> bounds =
1165 rc->ConditionalLb(integer_literal, objective_var_);
1166 if (bounds.first > node.false_objective) {
1167 ++num_rc_detected_;
1168 node.UpdateFalseObjective(bounds.first);
1169 }
1170 if (bounds.second > node.true_objective) {
1171 ++num_rc_detected_;
1172 node.UpdateTrueObjective(bounds.second);
1173 }
1174 }
1175}
1176
1177} // namespace sat
1178} // namespace operations_research
An helper class to share the code used by the different kind of search.
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.
int64_t num_lp_changes() const
This can serve as a timestamp to know if a saved basis is out of date.
T Get(std::function< T(const Model &)> f) const
Similar to Add() but this is const.
Definition model.h:93
const AssignmentInfo & Info(BooleanVariable var) const
Definition sat_base.h:463
const bool DEBUG_MODE
Definition macros.h:26
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::string FormatCounter(int64_t num)
Prints a positive number with separators for easier reading (ex: 1'348'065).
Definition util.cc:44
std::function< BooleanOrIntegerLiteral()> SatSolverHeuristic(Model *model)
Returns the BooleanOrIntegerLiteral advised by the underlying SAT solver.
std::function< BooleanOrIntegerLiteral()> LpPseudoCostHeuristic(Model *model)
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.
Information about a variable assignment.
Definition sat_base.h:242
int32_t trail_index
The index of this assignment in the trail.
Definition sat_base.h:263
static IntegerLiteral GreaterOrEqual(IntegerVariable i, IntegerValue bound)
static IntegerLiteral LowerOrEqual(IntegerVariable i, IntegerValue bound)
std::function< BooleanOrIntegerLiteral()> fixed_search
Fixed search, built from above building blocks.