// Copyright 2010-2025 Google LLC // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "ortools/sat/encoding.h" #include #include #include #include #include #include #include #include #include #include "absl/container/flat_hash_map.h" #include "absl/log/check.h" #include "absl/strings/str_cat.h" #include "absl/types/span.h" #include "ortools/base/stl_util.h" #include "ortools/sat/boolean_problem.pb.h" #include "ortools/sat/pb_constraint.h" #include "ortools/sat/sat_base.h" #include "ortools/sat/sat_parameters.pb.h" #include "ortools/sat/sat_solver.h" #include "ortools/sat/util.h" #include "ortools/util/strong_integers.h" namespace operations_research { namespace sat { EncodingNode EncodingNode::ConstantNode(Coefficient weight) { EncodingNode node; node.lb_ = 1; node.ub_ = 1; node.weight_lb_ = 0; node.weight_ = weight; return node; } EncodingNode EncodingNode::LiteralNode(Literal l, Coefficient weight) { EncodingNode node; node.lb_ = 0; node.weight_lb_ = 0; node.ub_ = 1; node.weight_ = weight; node.for_sorting_ = l.Variable(); node.literals_ = {l}; return node; } EncodingNode EncodingNode::GenericNode(int lb, int ub, std::function create_lit, Coefficient weight) { EncodingNode node; node.lb_ = lb; node.ub_ = ub; node.weight_lb_ = 0; node.create_lit_ = std::move(create_lit); node.weight_ = weight; node.literals_.push_back(node.create_lit_(lb)); // TODO(user): Not ideal, we should probably just provide index in the // original objective for sorting purpose. node.for_sorting_ = node.literals_[0].Variable(); return node; } void EncodingNode::InitializeFullNode(int n, EncodingNode* a, EncodingNode* b, SatSolver* solver) { CHECK(literals_.empty()) << "Already initialized"; CHECK_GT(n, 0); const BooleanVariable first_var_index(solver->NumVariables()); solver->SetNumVariables(solver->NumVariables() + n); for (int i = 0; i < n; ++i) { literals_.push_back(Literal(first_var_index + i, true)); if (i > 0) { solver->AddBinaryClause(literal(i - 1), literal(i).Negated()); } } lb_ = a->lb_ + b->lb_; ub_ = lb_ + n; depth_ = 1 + std::max(a->depth_, b->depth_); child_a_ = a; child_b_ = b; for_sorting_ = first_var_index; } void EncodingNode::InitializeAmoNode(absl::Span nodes, SatSolver* solver) { CHECK_GE(nodes.size(), 2); CHECK(literals_.empty()) << "Already initialized"; const BooleanVariable var = solver->NewBooleanVariable(); const Literal new_literal(var, true); literals_.push_back(new_literal); child_a_ = nullptr; child_b_ = nullptr; lb_ = 0; ub_ = 1; depth_ = 0; for_sorting_ = var; std::vector clause{new_literal.Negated()}; for (const EncodingNode* node : nodes) { // node_lit => new_lit. clause.push_back(node->literals_[0]); solver->AddBinaryClause(node->literals_[0].Negated(), new_literal); depth_ = std::max(node->depth_ + 1, depth_); for_sorting_ = std::min(for_sorting_, node->for_sorting_); } // If new_literal is true then one of the lit must be true. // Note that this is not needed for correctness though. solver->AddProblemClause(clause); } void EncodingNode::InitializeLazyNode(EncodingNode* a, EncodingNode* b, SatSolver* solver) { CHECK(literals_.empty()) << "Already initialized"; const BooleanVariable first_var_index(solver->NumVariables()); solver->SetNumVariables(solver->NumVariables() + 1); literals_.emplace_back(first_var_index, true); child_a_ = a; child_b_ = b; ub_ = a->ub_ + b->ub_; lb_ = a->lb_ + b->lb_; depth_ = 1 + std::max(a->depth_, b->depth_); // Merging the node of the same depth in order seems to help a bit. for_sorting_ = std::min(a->for_sorting_, b->for_sorting_); } void EncodingNode::InitializeLazyCoreNode(Coefficient weight, EncodingNode* a, EncodingNode* b) { CHECK(literals_.empty()) << "Already initialized"; child_a_ = a; child_b_ = b; ub_ = a->ub_ + b->ub_; weight_ = weight; weight_lb_ = a->lb_ + b->lb_; lb_ = weight_lb_ + 1; depth_ = 1 + std::max(a->depth_, b->depth_); // Merging the node of the same depth in order seems to help a bit. for_sorting_ = std::min(a->for_sorting_, b->for_sorting_); } bool EncodingNode::IncreaseCurrentUB(SatSolver* solver) { if (current_ub() == ub_) return false; if (create_lit_ != nullptr) { literals_.emplace_back(create_lit_(current_ub())); } else { CHECK_NE(solver, nullptr); literals_.emplace_back(BooleanVariable(solver->NumVariables()), true); solver->SetNumVariables(solver->NumVariables() + 1); } if (literals_.size() > 1) { solver->AddBinaryClause(literals_.back().Negated(), literals_[literals_.size() - 2]); } return true; } Coefficient EncodingNode::Reduce(const SatSolver& solver) { if (!literals_.empty()) { int i = 0; while (i < literals_.size() && solver.Assignment().LiteralIsTrue(literals_[i])) { ++i; ++lb_; } literals_.erase(literals_.begin(), literals_.begin() + i); while (!literals_.empty() && solver.Assignment().LiteralIsFalse(literals_.back())) { literals_.pop_back(); ub_ = lb_ + literals_.size(); } } if (weight_lb_ >= lb_) return Coefficient(0); const Coefficient result = Coefficient(lb_ - weight_lb_) * weight_; weight_lb_ = lb_; return result; } void EncodingNode::ApplyWeightUpperBound(Coefficient gap, SatSolver* solver) { CHECK_GT(weight_, 0); const Coefficient num_allowed = (gap / weight_); if (num_allowed > std::numeric_limits::max() / 2) return; const int new_size = std::max(0, (weight_lb_ - lb_) + static_cast(num_allowed.value())); if (size() <= new_size) return; for (int i = new_size; i < size(); ++i) { if (!solver->AddUnitClause(literal(i).Negated())) return; } literals_.resize(new_size); ub_ = lb_ + new_size; } void EncodingNode::TransformToBoolean(SatSolver* solver) { if (size() > 1) { for (int i = 1; i < size(); ++i) { if (!solver->AddUnitClause(literal(i).Negated())) return; } literals_.resize(1); ub_ = lb_ + 1; return; } if (current_ub() == ub_) return; // TODO(user): Avoid creating a Boolean just to fix it! IncreaseNodeSize(this, solver); CHECK_EQ(size(), 2); if (!solver->AddUnitClause(literal(1).Negated())) return; literals_.resize(1); ub_ = lb_ + 1; } bool EncodingNode::AssumptionIs(Literal other) const { DCHECK(!HasNoWeight()); const int index = weight_lb_ - lb_; return index < literals_.size() && literals_[index].Negated() == other; } Literal EncodingNode::GetAssumption(SatSolver* solver) { CHECK(!HasNoWeight()); const int index = weight_lb_ - lb_; CHECK_GE(index, 0) << "Not reduced?"; while (index >= literals_.size()) { IncreaseNodeSize(this, solver); } return literals_[index].Negated(); } void EncodingNode::IncreaseWeightLb() { CHECK_LT(weight_lb_ - lb_, literals_.size()); weight_lb_++; } bool EncodingNode::HasNoWeight() const { return weight_ == 0 || weight_lb_ >= ub_; } std::string EncodingNode::DebugString( const VariablesAssignment& assignment) const { std::string result; absl::StrAppend(&result, "depth:", depth_); absl::StrAppend(&result, " [", lb_, ",", lb_ + literals_.size(), "]"); absl::StrAppend(&result, " ub:", ub_); absl::StrAppend(&result, " weight:", weight_.value()); absl::StrAppend(&result, " weight_lb:", weight_lb_); absl::StrAppend(&result, " values:"); const size_t limit = 20; int value = 0; for (int i = 0; i < std::min(literals_.size(), limit); ++i) { char c = '?'; if (assignment.LiteralIsTrue(literals_[i])) { c = '1'; value = i + 1; } else if (assignment.LiteralIsFalse(literals_[i])) { c = '0'; } result += c; } absl::StrAppend(&result, " val:", lb_ + value); return result; } EncodingNode LazyMerge(EncodingNode* a, EncodingNode* b, SatSolver* solver) { EncodingNode n; n.InitializeLazyNode(a, b, solver); solver->AddBinaryClause(a->literal(0).Negated(), n.literal(0)); solver->AddBinaryClause(b->literal(0).Negated(), n.literal(0)); solver->AddTernaryClause(n.literal(0).Negated(), a->literal(0), b->literal(0)); return n; } void IncreaseNodeSize(EncodingNode* node, SatSolver* solver) { if (!node->IncreaseCurrentUB(solver)) return; std::vector to_process; to_process.push_back(node); // Only one side of the constraint is mandatory (the one propagating the ones // to the top of the encoding tree), and it seems more efficient not to encode // the other side. // // TODO(user): Experiment more. const bool complete_encoding = false; while (!to_process.empty()) { EncodingNode* n = to_process.back(); EncodingNode* a = n->child_a(); EncodingNode* b = n->child_b(); to_process.pop_back(); // Integer leaf node. if (a == nullptr) continue; CHECK_NE(solver, nullptr); // Note that since we were able to increase its size, n must have children. // n->GreaterThan(target) is the new literal of n. CHECK(a != nullptr); CHECK(b != nullptr); const int target = n->current_ub() - 1; // Add a literal to a if needed. // That is, now that the node n can go up to it new current_ub, if we need // to increase the current_ub of a. if (a->current_ub() != a->ub()) { CHECK_GE(a->current_ub() - 1 + b->lb(), target - 1); if (a->current_ub() - 1 + b->lb() < target) { CHECK(a->IncreaseCurrentUB(solver)); to_process.push_back(a); } } // Add a literal to b if needed. if (b->current_ub() != b->ub()) { CHECK_GE(b->current_ub() - 1 + a->lb(), target - 1); if (b->current_ub() - 1 + a->lb() < target) { CHECK(b->IncreaseCurrentUB(solver)); to_process.push_back(b); } } // Wire the new literal of n correctly with its two children. for (int ia = a->lb(); ia < a->current_ub(); ++ia) { const int ib = target - ia; if (complete_encoding && ib >= b->lb() && ib < b->current_ub()) { // if x <= ia and y <= ib then x + y <= ia + ib. solver->AddTernaryClause(n->GreaterThan(target).Negated(), a->GreaterThan(ia), b->GreaterThan(ib)); } if (complete_encoding && ib == b->ub()) { solver->AddBinaryClause(n->GreaterThan(target).Negated(), a->GreaterThan(ia)); } if (ib - 1 == b->lb() - 1) { solver->AddBinaryClause(n->GreaterThan(target), a->GreaterThan(ia).Negated()); } if ((ib - 1) >= b->lb() && (ib - 1) < b->current_ub()) { // if x > ia and y > ib - 1 then x + y > ia + ib. solver->AddTernaryClause(n->GreaterThan(target), a->GreaterThan(ia).Negated(), b->GreaterThan(ib - 1).Negated()); } } // Case ia = a->lb() - 1; a->GreaterThan(ia) always true. { const int ib = target - (a->lb() - 1); if ((ib - 1) == b->lb() - 1) { if (!solver->AddUnitClause(n->GreaterThan(target))) return; } if ((ib - 1) >= b->lb() && (ib - 1) < b->current_ub()) { solver->AddBinaryClause(n->GreaterThan(target), b->GreaterThan(ib - 1).Negated()); } } // case ia == a->ub; a->GreaterThan(ia) always false. { const int ib = target - a->ub(); if (complete_encoding && ib >= b->lb() && ib < b->current_ub()) { solver->AddBinaryClause(n->GreaterThan(target).Negated(), b->GreaterThan(ib)); } if (ib == b->ub()) { if (!solver->AddUnitClause(n->GreaterThan(target).Negated())) return; } } } } EncodingNode FullMerge(Coefficient upper_bound, EncodingNode* a, EncodingNode* b, SatSolver* solver) { EncodingNode n; const int size = std::min(Coefficient(a->size() + b->size()), upper_bound).value(); n.InitializeFullNode(size, a, b, solver); for (int ia = 0; ia < a->size(); ++ia) { if (ia + b->size() < size) { solver->AddBinaryClause(n.literal(ia + b->size()).Negated(), a->literal(ia)); } if (ia < size) { solver->AddBinaryClause(n.literal(ia), a->literal(ia).Negated()); } else { // Fix the variable to false because of the given upper_bound. if (!solver->AddUnitClause(a->literal(ia).Negated())) return n; } } for (int ib = 0; ib < b->size(); ++ib) { if (ib + a->size() < size) { solver->AddBinaryClause(n.literal(ib + a->size()).Negated(), b->literal(ib)); } if (ib < size) { solver->AddBinaryClause(n.literal(ib), b->literal(ib).Negated()); } else { // Fix the variable to false because of the given upper_bound. if (!solver->AddUnitClause(b->literal(ib).Negated())) return n; } } for (int ia = 0; ia < a->size(); ++ia) { for (int ib = 0; ib < b->size(); ++ib) { if (ia + ib < size) { // if x <= ia and y <= ib, then x + y <= ia + ib. solver->AddTernaryClause(n.literal(ia + ib).Negated(), a->literal(ia), b->literal(ib)); } if (ia + ib + 1 < size) { // if x > ia and y > ib, then x + y > ia + ib + 1. solver->AddTernaryClause(n.literal(ia + ib + 1), a->literal(ia).Negated(), b->literal(ib).Negated()); } else { solver->AddBinaryClause(a->literal(ia).Negated(), b->literal(ib).Negated()); } } } return n; } EncodingNode* MergeAllNodesWithDeque(Coefficient upper_bound, const std::vector& nodes, SatSolver* solver, std::deque* repository) { std::deque dq(nodes.begin(), nodes.end()); while (dq.size() > 1) { EncodingNode* a = dq.front(); dq.pop_front(); EncodingNode* b = dq.front(); dq.pop_front(); repository->push_back(FullMerge(upper_bound, a, b, solver)); dq.push_back(&repository->back()); } return dq.front(); } namespace { struct SortEncodingNodePointers { bool operator()(EncodingNode* a, EncodingNode* b) const { return *a < *b; } }; } // namespace EncodingNode* LazyMergeAllNodeWithPQAndIncreaseLb( Coefficient weight, const std::vector& nodes, SatSolver* solver, std::deque* repository) { std::priority_queue, SortEncodingNodePointers> pq(nodes.begin(), nodes.end()); while (pq.size() > 2) { EncodingNode* a = pq.top(); pq.pop(); EncodingNode* b = pq.top(); pq.pop(); repository->push_back(LazyMerge(a, b, solver)); pq.push(&repository->back()); } CHECK_EQ(pq.size(), 2); EncodingNode* a = pq.top(); pq.pop(); EncodingNode* b = pq.top(); pq.pop(); repository->push_back(EncodingNode()); EncodingNode* n = &repository->back(); n->InitializeLazyCoreNode(weight, a, b); solver->AddBinaryClause(a->literal(0), b->literal(0)); return n; } namespace { bool EncodingNodeByWeight(const EncodingNode* a, const EncodingNode* b) { return a->weight() < b->weight(); } bool EncodingNodeByDepth(const EncodingNode* a, const EncodingNode* b) { return a->depth() < b->depth(); } } // namespace void ReduceNodes(Coefficient upper_bound, Coefficient* lower_bound, std::vector* nodes, SatSolver* solver) { // Remove the left-most variables fixed to one from each node. // Also update the lower_bound. Note that Reduce() needs the solver to be // at the root node in order to work. solver->Backtrack(0); for (EncodingNode* n : *nodes) { *lower_bound += n->Reduce(*solver); } // Fix the nodes right-most variables that are above the gap. // If we closed the problem, we abort and return and empty vector. if (upper_bound != kCoefficientMax) { const Coefficient gap = upper_bound - *lower_bound; if (gap < 0) { nodes->clear(); return; } for (EncodingNode* n : *nodes) { n->ApplyWeightUpperBound(gap, solver); } } // Remove the empty nodes. nodes->erase(std::remove_if(nodes->begin(), nodes->end(), [](EncodingNode* a) { return a->HasNoWeight(); }), nodes->end()); // Sort the nodes. switch (solver->parameters().max_sat_assumption_order()) { case SatParameters::DEFAULT_ASSUMPTION_ORDER: break; case SatParameters::ORDER_ASSUMPTION_BY_DEPTH: std::sort(nodes->begin(), nodes->end(), EncodingNodeByDepth); break; case SatParameters::ORDER_ASSUMPTION_BY_WEIGHT: std::sort(nodes->begin(), nodes->end(), EncodingNodeByWeight); break; } if (solver->parameters().max_sat_reverse_assumption_order()) { // TODO(user): with DEFAULT_ASSUMPTION_ORDER, this will lead to a somewhat // weird behavior, since we will reverse the nodes at each iteration... std::reverse(nodes->begin(), nodes->end()); } } std::vector ExtractAssumptions(Coefficient stratified_lower_bound, const std::vector& nodes, SatSolver* solver) { // Extract the assumptions from the nodes. std::vector assumptions; for (EncodingNode* n : nodes) { if (n->weight() >= stratified_lower_bound) { assumptions.push_back(n->GetAssumption(solver)); } } return assumptions; } Coefficient ComputeCoreMinWeight(const std::vector& nodes, absl::Span core) { Coefficient min_weight = kCoefficientMax; int index = 0; for (int i = 0; i < core.size(); ++i) { for (; index < nodes.size() && !nodes[index]->AssumptionIs(core[i]); ++index) { } CHECK_LT(index, nodes.size()); min_weight = std::min(min_weight, nodes[index]->weight()); } return min_weight; } Coefficient MaxNodeWeightSmallerThan(const std::vector& nodes, Coefficient upper_bound) { Coefficient result(0); for (EncodingNode* n : nodes) { CHECK_GT(n->weight(), 0); if (n->weight() < upper_bound) { result = std::max(result, n->weight()); } } return result; } bool ObjectiveEncoder::ProcessCore(absl::Span core, Coefficient min_weight, Coefficient gap, std::string* info) { // Backtrack to be able to add new constraints. if (!sat_solver_->ResetToLevelZero()) return false; if (core.size() == 1) { return sat_solver_->AddUnitClause(core[0].Negated()); } // Remove from nodes the EncodingNode in the core and put them in to_merge. std::vector to_merge; { int index = 0; std::vector new_nodes; for (int i = 0; i < core.size(); ++i) { // Since the nodes appear in order in the core, we can find the // relevant "objective" variable efficiently with a simple linear scan // in the nodes vector (done with index). for (; !nodes_[index]->AssumptionIs(core[i]); ++index) { CHECK_LT(index, nodes_.size()); new_nodes.push_back(nodes_[index]); } CHECK_LT(index, nodes_.size()); EncodingNode* node = nodes_[index]; // TODO(user): propagate proper ub first. if (alternative_encoding_ && node->ub() > node->lb() + 1) { // We can distinguish the first literal of the node. // By not counting its weight in node, and creating a new node for it. node->IncreaseWeightLb(); new_nodes.push_back(node); // Now keep processing with this new node instead. repository_.push_back( EncodingNode::LiteralNode(core[i].Negated(), node->weight())); repository_.back().set_depth(node->depth()); node = &repository_.back(); } to_merge.push_back(node); // Special case if the weight > min_weight. we keep it, but reduce its // cost. This is the same "trick" as in WPM1 used to deal with weight. // We basically split a clause with a larger weight in two identical // clauses, one with weight min_weight that will be merged and one with // the remaining weight. if (node->weight() > min_weight) { node->set_weight(node->weight() - min_weight); new_nodes.push_back(node); } ++index; } for (; index < nodes_.size(); ++index) { new_nodes.push_back(nodes_[index]); } nodes_ = new_nodes; } // Are the literal in amo relationship? // - If min_weight is large enough, we can infer that. // - If the size is small we can infer this via propagation. bool in_exactly_one = (2 * min_weight) > gap; // Amongst the node to merge, if many are boolean nodes in an "at most one" // relationship, it is super advantageous to exploit it during merging as we // can regroup all nodes from an at most one in a single new node with a depth // of 1. if (!in_exactly_one) { // Collect "boolean nodes". std::vector bool_nodes; absl::flat_hash_map node_indices; for (int i = 0; i < to_merge.size(); ++i) { const EncodingNode& node = *to_merge[i]; if (node.size() != 1) continue; if (node.ub() != node.lb() + 1) continue; if (node.weight_lb() != node.lb()) continue; if (node_indices.contains(node.literal(0).Index())) continue; node_indices[node.literal(0).Index()] = i; bool_nodes.push_back(node.literal(0)); } // For "small" core, with O(n) full propagation, we can discover possible // at most ones. This is a bit costly but can significantly reduce the // number of Booleans needed and has a good positive impact. std::vector buffer; std::vector> decomposition; if (params_.core_minimization_level() > 1 && bool_nodes.size() < 300 && bool_nodes.size() > 1) { const auto& assignment = sat_solver_->Assignment(); const int size = bool_nodes.size(); std::vector> graph(size); for (int i = 0; i < size; ++i) { if (!sat_solver_->ResetToLevelZero()) return false; if (!sat_solver_->EnqueueDecisionIfNotConflicting(bool_nodes[i])) { // TODO(user): this node is closed and can be removed from the core. continue; } for (int j = 0; j < size; ++j) { if (i == j) continue; if (assignment.LiteralIsFalse(bool_nodes[j])) { graph[i].push_back(j); // Unit propagation is not always symmetric. graph[j].push_back(i); } // TODO(user): If assignment.LiteralIsTrue(bool_nodes[j]) We can // minimize the core here by removing bool_nodes[i] from it. Note // however that since we already minimized the core, this is // unlikely to happen. } } if (!sat_solver_->ResetToLevelZero()) return false; for (std::vector& adj : graph) { gtl::STLSortAndRemoveDuplicates(&adj); } const std::vector> index_decompo = AtMostOneDecomposition(graph, *random_, &buffer); // Convert. std::vector new_order; for (const int i : buffer) new_order.push_back(bool_nodes[i]); bool_nodes = new_order; for (const auto span : index_decompo) { if (span.size() == 1) continue; decomposition.push_back(absl::MakeSpan( bool_nodes.data() + (span.data() - buffer.data()), span.size())); } } else { decomposition = implications_->HeuristicAmoPartition(&bool_nodes); } // Same case as above, all the nodes in the core are in a exactly_one. if (decomposition.size() == 1 && decomposition[0].size() == core.size()) { in_exactly_one = true; } int num_in_decompo = 0; if (!in_exactly_one) { for (const auto amo : decomposition) { num_in_decompo += amo.size(); // Extract Amo nodes and set them to nullptr in to_merge. std::vector amo_nodes; for (const Literal l : amo) { const int index = node_indices.at(l.Index()); amo_nodes.push_back(to_merge[index]); to_merge[index] = nullptr; } // Create the new node with proper constraints and weight. repository_.push_back(EncodingNode()); EncodingNode* n = &repository_.back(); n->InitializeAmoNode(amo_nodes, sat_solver_); n->set_weight(min_weight); to_merge.push_back(n); } if (num_in_decompo > 0) { absl::StrAppend(info, " amo:", decomposition.size(), " lit:", num_in_decompo); } // Clean-up to_merge. int new_size = 0; for (EncodingNode* node : to_merge) { if (node == nullptr) continue; to_merge[new_size++] = node; } to_merge.resize(new_size); } } // If all the literal of the core are in at_most_one, the core is actually an // exactly_one. We just subtracted the min_cost above. We just have to enqueue // a constant node with min_weight for the rest of the code to work. if (in_exactly_one) { // Tricky: We need to enforce an upper bound of 1 on the nodes. for (EncodingNode* node : to_merge) { node->TransformToBoolean(sat_solver_); } absl::StrAppend(info, " exo"); repository_.push_back(EncodingNode::ConstantNode(min_weight)); nodes_.push_back(&repository_.back()); // The negation of the literal in the core are in exactly one. // TODO(user): If we infered the exactly one from the binary implication // graph, there is no need to add the amo since it is already there. std::vector cst; cst.reserve(core.size()); for (const Literal l : core) { cst.emplace_back(l.Negated(), Coefficient(1)); } sat_solver_->AddLinearConstraint( /*use_lower_bound=*/true, Coefficient(1), /*use_upper_bound=*/true, Coefficient(1), &cst); return !sat_solver_->ModelIsUnsat(); } nodes_.push_back(LazyMergeAllNodeWithPQAndIncreaseLb( min_weight, to_merge, sat_solver_, &repository_)); absl::StrAppend(info, " d:", nodes_.back()->depth()); return !sat_solver_->ModelIsUnsat(); } } // namespace sat } // namespace operations_research