// Copyright 2010-2014 Google // 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 "sat/optimization.h" #include #include #include "google/protobuf/descriptor.h" #include "sat/encoding.h" #include "sat/util.h" namespace operations_research { namespace sat { namespace { // Used to log messages to stdout or to the normal logging framework according // to the given LogBehavior value. class Logger { public: explicit Logger(LogBehavior v) : use_stdout_(v == STDOUT_LOG) {} void Log(std::string message) { if (use_stdout_) { printf("%s\n", message.c_str()); } else { LOG(INFO) << message; } } private: bool use_stdout_; }; // Outputs the current objective value in the cnf output format. // Note that this function scale the given objective. std::string CnfObjectiveLine(const LinearBooleanProblem& problem, Coefficient objective) { const double scaled_objective = AddOffsetAndScaleObjectiveValue(problem, objective); return StringPrintf("o %lld", static_cast(scaled_objective)); } struct LiteralWithCoreIndex { LiteralWithCoreIndex(Literal l, int i) : literal(l), core_index(i) {} Literal literal; int core_index; }; // Deletes the given indices from a vector. The given indices must be sorted in // increasing order. The order of the non-deleted entries in the vector is // preserved. template void DeleteVectorIndices(const std::vector& indices, Vector* v) { int new_size = 0; int indices_index = 0; for (int i = 0; i < v->size(); ++i) { if (indices_index < indices.size() && i == indices[indices_index]) { ++indices_index; } else { (*v)[new_size] = (*v)[i]; ++new_size; } } v->resize(new_size); } // In the Fu & Malik algorithm (or in WPM1), when two cores overlap, we // artifically introduce symmetries. More precisely: // // The picture below shows two cores with index 0 and 1, with one blocking // variable per '-' and with the variables ordered from left to right (by their // assumptions index). The blocking variables will be the one added to "relax" // the core for the next iteration. // // 1: ------------------------------- // 0: ------------------------------------ // // The 2 following assignment of the blocking variables are equivalent. // Remember that exactly one blocking variable per core must be assigned to 1. // // 1: ----------------------1-------- // 0: --------1--------------------------- // // and // // 1: ---------------------------1--- // 0: ---1-------------------------------- // // This class allows to add binary constraints excluding the second possibility. // Basically, each time a new core is added, if two of its blocking variables // (b1, b2) have the same assumption index of two blocking variables from // another core (c1, c2), then we forbid the assignment c1 true and b2 true. // // Reference: C Ansótegui, ML Bonet, J Levy, "Sat-based maxsat algorithms", // Artificial Intelligence, 2013 - Elsevier. class FuMalikSymmetryBreaker { public: FuMalikSymmetryBreaker() {} // Must be called before a new core is processed. void StartResolvingNewCore(int new_core_index) { literal_by_core_.resize(new_core_index); for (int i = 0; i < new_core_index; ++i) { literal_by_core_[i].clear(); } } // This should be called for each blocking literal b of the new core. The // assumption_index identify the soft clause associated to the given blocking // literal. Note that between two StartResolvingNewCore() calls, // ProcessLiteral() is assumed to be called with different assumption_index. // // Changing the order of the calls will not change the correctness, but will // change the symmetry-breaking clause produced. // // Returns a set of literals which can't be true at the same time as b (under // symmetry breaking). std::vector ProcessLiteral(int assumption_index, Literal b) { if (assumption_index >= info_by_assumption_index_.size()) { info_by_assumption_index_.resize(assumption_index + 1); } // Compute the function result. // info_by_assumption_index_[assumption_index] will contain all the pairs // (blocking_literal, core) of the previous resolved cores at the same // assumption index as b. std::vector result; for (LiteralWithCoreIndex data : info_by_assumption_index_[assumption_index]) { // literal_by_core_ will contain all the blocking literal of a given core // with an assumption_index that was used in one of the ProcessLiteral() // calls since the last StartResolvingNewCore(). // // Note that there can be only one such literal by core, so we will not // add duplicates. result.insert(result.end(), literal_by_core_[data.core_index].begin(), literal_by_core_[data.core_index].end()); } // Update the internal data structure. for (LiteralWithCoreIndex data : info_by_assumption_index_[assumption_index]) { literal_by_core_[data.core_index].push_back(data.literal); } info_by_assumption_index_[assumption_index].push_back( LiteralWithCoreIndex(b, literal_by_core_.size())); return result; } // Deletes the given assumption indices. void DeleteIndices(const std::vector& indices) { DeleteVectorIndices(indices, &info_by_assumption_index_); } // This is only used in WPM1 to forget all the information related to a given // assumption_index. void ClearInfo(int assumption_index) { CHECK_LE(assumption_index, info_by_assumption_index_.size()); info_by_assumption_index_[assumption_index].clear(); } // This is only used in WPM1 when a new assumption_index is created. void AddInfo(int assumption_index, Literal b) { CHECK_GE(assumption_index, info_by_assumption_index_.size()); info_by_assumption_index_.resize(assumption_index + 1); info_by_assumption_index_[assumption_index].push_back( LiteralWithCoreIndex(b, literal_by_core_.size())); } private: std::vector> info_by_assumption_index_; std::vector> literal_by_core_; DISALLOW_COPY_AND_ASSIGN(FuMalikSymmetryBreaker); }; } // namespace void MinimizeCore(SatSolver* solver, std::vector* core) { std::vector temp = *core; std::reverse(temp.begin(), temp.end()); solver->Backtrack(0); // Note that this Solve() is really fast, since the solver should detect that // the assumptions are unsat with unit propagation only. This is just a // convenient way to remove assumptions that are propagated by the one before // them. const SatSolver::Status status = solver->ResetAndSolveWithGivenAssumptions(temp); if (status != SatSolver::ASSUMPTIONS_UNSAT) { if (status != SatSolver::LIMIT_REACHED) { // This should almost never happen, but it is not impossible. The reason // is that the solver may delete some learned clauses required by the unit // propagation to show that the core is unsat. LOG(WARNING) << "This should only happen rarely! otherwise, investigate. " << "Returned status is " << SatStatusString(status); } return; } temp = solver->GetLastIncompatibleDecisions(); if (temp.size() < core->size()) { VLOG(1) << "old core size " << core->size(); std::reverse(temp.begin(), temp.end()); *core = temp; } } // This algorithm works by exploiting the unsat core returned by the SAT solver // when the problem is UNSAT. It starts by trying to solve the decision problem // where all the objective variables are set to their value with minimal cost, // and relax in each step some of these fixed variables until the problem // becomes satisfiable. SatSolver::Status SolveWithFuMalik(LogBehavior log, const LinearBooleanProblem& problem, SatSolver* solver, std::vector* solution) { Logger logger(log); FuMalikSymmetryBreaker symmetry; // blocking_clauses will contains a set of clauses that are currently added to // the initial problem. // // Initially, each clause just contains a literal associated to an objective // variable with non-zero cost. Setting all these literals to true will lead // to the lowest possible objective. // // During the algorithm, "blocking" literals will be added to each clause. // Moreover each clause will contain an extra "assumption" literal stored in // the separate assumptions vector (in its negated form). // // The meaning of a given clause will always be: // If the assumption literal and all blocking literals are false, then the // "objective" literal (which is the first one in the clause) must be true. // When the "objective" literal is true, its variable (which have a non-zero // cost) is set to the value that minimize the objective cost. // // ex: If a variable "x" as a cost of 3, its cost contribution is smaller when // it is set to false (since it will contribute to zero instead of 3). std::vector> blocking_clauses; std::vector assumptions; // Initialize blocking_clauses and assumptions. const LinearObjective& objective = problem.objective(); CHECK_GT(objective.coefficients_size(), 0); const Coefficient unique_objective_coeff(std::abs(objective.coefficients(0))); for (int i = 0; i < objective.literals_size(); ++i) { CHECK_EQ(std::abs(objective.coefficients(i)), unique_objective_coeff) << "The basic Fu & Malik algorithm needs constant objective coeffs."; const Literal literal(objective.literals(i)); // We want to minimize the cost when this literal is true. const Literal min_literal = objective.coefficients(i) > 0 ? literal.Negated() : literal; blocking_clauses.push_back(std::vector(1, min_literal)); // Note that initialy, we do not create any extra variables. assumptions.push_back(min_literal); } // Print the number of variable with a non-zero cost. logger.Log(StringPrintf("c #weights:%zu #vars:%d #constraints:%d", assumptions.size(), problem.num_variables(), problem.constraints_size())); // Starts the algorithm. Each loop will solve the problem under the given // assumptions, and if unsat, will relax exactly one of the objective // variables (from the unsat core) to be in its "costly" state. When the // algorithm terminates, the number of iterations is exactly the minimal // objective value. for (int iter = 0;; ++iter) { const SatSolver::Status result = solver->ResetAndSolveWithGivenAssumptions(assumptions); if (result == SatSolver::MODEL_SAT) { ExtractAssignment(problem, *solver, solution); Coefficient objective = ComputeObjectiveValue(problem, *solution); logger.Log(CnfObjectiveLine(problem, objective)); return SatSolver::MODEL_SAT; } if (result != SatSolver::ASSUMPTIONS_UNSAT) return result; // The interesting case: we have an unsat core. // // We need to add new "blocking" variables b_i for all the objective // variable appearing in the core. Moreover, we will only relax as little // as possible (to not miss the optimal), so we will enforce that the sum // of the b_i is exactly one. std::vector core = solver->GetLastIncompatibleDecisions(); MinimizeCore(solver, &core); solver->Backtrack(0); // Print the search progress. logger.Log(StringPrintf("c iter:%d core:%zu", iter, core.size())); // Special case for a singleton core. if (core.size() == 1) { // Find the index of the "objective" variable that need to be fixed in // its "costly" state. const int index = std::find(assumptions.begin(), assumptions.end(), core[0]) - assumptions.begin(); CHECK_LT(index, assumptions.size()); // Fix it. We also fix all the associated blocking variables if any. if (!solver->AddUnitClause(core[0].Negated())) { return SatSolver::MODEL_UNSAT; } for (Literal b : blocking_clauses[index]) { if (!solver->AddUnitClause(b.Negated())) return SatSolver::MODEL_UNSAT; } // Erase this entry from the current "objective" std::vector to_delete(1, index); DeleteVectorIndices(to_delete, &assumptions); DeleteVectorIndices(to_delete, &blocking_clauses); symmetry.DeleteIndices(to_delete); } else { symmetry.StartResolvingNewCore(iter); // We will add 2 * |core.size()| variables. const int old_num_variables = solver->NumVariables(); if (core.size() == 2) { // Special case. If core.size() == 2, we can use only one blocking // variable (the other one beeing its negation). This actually do happen // quite often in practice, so it is worth it. solver->SetNumVariables(old_num_variables + 3); } else { solver->SetNumVariables(old_num_variables + 2 * core.size()); } // Temporary vectors for the constraint (sum new blocking variable == 1). std::vector at_most_one_constraint; std::vector at_least_one_constraint; // This will be set to false if the problem becomes unsat while adding a // new clause. This is unlikely, but may be possible. bool ok = true; // Loop over the core. int index = 0; for (int i = 0; i < core.size(); ++i) { // Since the assumptions appear in order in the core, we can find the // relevant "objective" variable efficiently with a simple linear scan // in the assumptions vector (done with index). index = std::find(assumptions.begin() + index, assumptions.end(), core[i]) - assumptions.begin(); CHECK_LT(index, assumptions.size()); // The new blocking and assumption variables for this core entry. const Literal a(BooleanVariable(old_num_variables + i), true); Literal b(BooleanVariable(old_num_variables + core.size() + i), true); if (core.size() == 2) { b = Literal(BooleanVariable(old_num_variables + 2), true); if (i == 1) b = b.Negated(); } // Symmetry breaking clauses. for (Literal l : symmetry.ProcessLiteral(index, b)) { ok &= solver->AddBinaryClause(l.Negated(), b.Negated()); } // Note(user): There is more than one way to encode the algorithm in // SAT. Here we "delete" the old blocking clause and add a new one. In // the WPM1 algorithm below, the blocking clause is decomposed into // 3-SAT and we don't need to delete anything. // First, fix the old "assumption" variable to false, which has the // effect of deleting the old clause from the solver. if (assumptions[index].Variable() >= problem.num_variables()) { CHECK(solver->AddUnitClause(assumptions[index].Negated())); } // Add the new blocking variable. blocking_clauses[index].push_back(b); // Add the new clause to the solver. Temporary including the // assumption, but removing it right afterwards. blocking_clauses[index].push_back(a); ok &= solver->AddProblemClause(blocking_clauses[index]); blocking_clauses[index].pop_back(); // For the "== 1" constraint on the blocking literals. at_most_one_constraint.push_back(LiteralWithCoeff(b, 1.0)); at_least_one_constraint.push_back(b); // The new assumption variable replace the old one. assumptions[index] = a.Negated(); } // Add the "<= 1" side of the "== 1" constraint. ok &= solver->AddLinearConstraint(false, Coefficient(0), true, Coefficient(1.0), &at_most_one_constraint); // TODO(user): The algorithm does not really need the >= 1 side of this // constraint. Initial investigation shows that it doesn't really help, // but investigate more. if (/* DISABLES CODE */ (false)) { ok &= solver->AddProblemClause(at_least_one_constraint); } if (!ok) { LOG(INFO) << "Unsat while adding a clause."; return SatSolver::MODEL_UNSAT; } } } } SatSolver::Status SolveWithWPM1(LogBehavior log, const LinearBooleanProblem& problem, SatSolver* solver, std::vector* solution) { Logger logger(log); FuMalikSymmetryBreaker symmetry; // The curent lower_bound on the cost. // It will be correct after the initialization. Coefficient lower_bound(static_cast(problem.objective().offset())); Coefficient upper_bound(kint64max); // The assumption literals and their associated cost. std::vector assumptions; std::vector costs; std::vector reference; // Initialization. const LinearObjective& objective = problem.objective(); CHECK_GT(objective.coefficients_size(), 0); for (int i = 0; i < objective.literals_size(); ++i) { const Literal literal(objective.literals(i)); const Coefficient coeff(objective.coefficients(i)); // We want to minimize the cost when the assumption is true. // Note that initially, we do not create any extra variables. if (coeff > 0) { assumptions.push_back(literal.Negated()); costs.push_back(coeff); } else { assumptions.push_back(literal); costs.push_back(-coeff); lower_bound += coeff; } } reference = assumptions; // This is used by the "stratified" approach. Coefficient stratified_lower_bound = *std::max_element(costs.begin(), costs.end()); // Print the number of variables with a non-zero cost. logger.Log(StringPrintf("c #weights:%zu #vars:%d #constraints:%d", assumptions.size(), problem.num_variables(), problem.constraints_size())); for (int iter = 0;; ++iter) { // This is called "hardening" in the literature. // Basically, we know that there is only hardening_threshold weight left // to distribute, so any assumption with a greater cost than this can never // be false. We fix it instead of treating it as an assumption. solver->Backtrack(0); const Coefficient hardening_threshold = upper_bound - lower_bound; CHECK_GE(hardening_threshold, 0); std::vector to_delete; int num_above_threshold = 0; for (int i = 0; i < assumptions.size(); ++i) { if (costs[i] > hardening_threshold) { if (!solver->AddUnitClause(assumptions[i])) { return SatSolver::MODEL_UNSAT; } to_delete.push_back(i); ++num_above_threshold; } else { // This impact the stratification heuristic. if (solver->Assignment().LiteralIsTrue(assumptions[i])) { to_delete.push_back(i); } } } if (!to_delete.empty()) { logger.Log(StringPrintf("c fixed %zu assumptions, %d with cost > %lld", to_delete.size(), num_above_threshold, hardening_threshold.value())); DeleteVectorIndices(to_delete, &assumptions); DeleteVectorIndices(to_delete, &costs); DeleteVectorIndices(to_delete, &reference); symmetry.DeleteIndices(to_delete); } // This is the "stratification" part. // Extract the assumptions with a cost >= stratified_lower_bound. std::vector assumptions_subset; for (int i = 0; i < assumptions.size(); ++i) { if (costs[i] >= stratified_lower_bound) { assumptions_subset.push_back(assumptions[i]); } } const SatSolver::Status result = solver->ResetAndSolveWithGivenAssumptions(assumptions_subset); if (result == SatSolver::MODEL_SAT) { // If not all assumptions where taken, continue with a lower stratified // bound. Otherwise we have an optimal solution! // // TODO(user): Try more advanced variant where the bound is lowered by // more than this minimal amount. const Coefficient old_lower_bound = stratified_lower_bound; for (Coefficient cost : costs) { if (cost < old_lower_bound) { if (stratified_lower_bound == old_lower_bound || cost > stratified_lower_bound) { stratified_lower_bound = cost; } } } ExtractAssignment(problem, *solver, solution); DCHECK(IsAssignmentValid(problem, *solution)); const Coefficient objective_offset( static_cast(problem.objective().offset())); const Coefficient objective = ComputeObjectiveValue(problem, *solution); if (objective + objective_offset < upper_bound) { logger.Log(CnfObjectiveLine(problem, objective)); upper_bound = objective + objective_offset; } if (stratified_lower_bound < old_lower_bound) continue; return SatSolver::MODEL_SAT; } if (result != SatSolver::ASSUMPTIONS_UNSAT) return result; // The interesting case: we have an unsat core. // // We need to add new "blocking" variables b_i for all the objective // variables appearing in the core. Moreover, we will only relax as little // as possible (to not miss the optimal), so we will enforce that the sum // of the b_i is exactly one. std::vector core = solver->GetLastIncompatibleDecisions(); MinimizeCore(solver, &core); solver->Backtrack(0); // Compute the min cost of all the assertions in the core. // The lower bound will be updated by that much. Coefficient min_cost = kCoefficientMax; { int index = 0; for (int i = 0; i < core.size(); ++i) { index = std::find(assumptions.begin() + index, assumptions.end(), core[i]) - assumptions.begin(); CHECK_LT(index, assumptions.size()); min_cost = std::min(min_cost, costs[index]); } } lower_bound += min_cost; // Print the search progress. logger.Log( StringPrintf("c iter:%d core:%zu lb:%lld min_cost:%lld strat:%lld", iter, core.size(), lower_bound.value(), min_cost.value(), stratified_lower_bound.value())); // This simple line helps a lot on the packup-wpms instances! // // TODO(user): That was because of a bug before in the way // stratified_lower_bound was decremented, not sure it helps that much now. if (min_cost > stratified_lower_bound) { stratified_lower_bound = min_cost; } // Special case for a singleton core. if (core.size() == 1) { // Find the index of the "objective" variable that need to be fixed in // its "costly" state. const int index = std::find(assumptions.begin(), assumptions.end(), core[0]) - assumptions.begin(); CHECK_LT(index, assumptions.size()); // Fix it. if (!solver->AddUnitClause(core[0].Negated())) { return SatSolver::MODEL_UNSAT; } // Erase this entry from the current "objective". std::vector to_delete(1, index); DeleteVectorIndices(to_delete, &assumptions); DeleteVectorIndices(to_delete, &costs); DeleteVectorIndices(to_delete, &reference); symmetry.DeleteIndices(to_delete); } else { symmetry.StartResolvingNewCore(iter); // We will add 2 * |core.size()| variables. const int old_num_variables = solver->NumVariables(); if (core.size() == 2) { // Special case. If core.size() == 2, we can use only one blocking // variable (the other one beeing its negation). This actually do happen // quite often in practice, so it is worth it. solver->SetNumVariables(old_num_variables + 3); } else { solver->SetNumVariables(old_num_variables + 2 * core.size()); } // Temporary vectors for the constraint (sum new blocking variable == 1). std::vector at_most_one_constraint; std::vector at_least_one_constraint; // This will be set to false if the problem becomes unsat while adding a // new clause. This is unlikely, but may be possible. bool ok = true; // Loop over the core. int index = 0; for (int i = 0; i < core.size(); ++i) { // Since the assumptions appear in order in the core, we can find the // relevant "objective" variable efficiently with a simple linear scan // in the assumptions vector (done with index). index = std::find(assumptions.begin() + index, assumptions.end(), core[i]) - assumptions.begin(); CHECK_LT(index, assumptions.size()); // The new blocking and assumption variables for this core entry. const Literal a(BooleanVariable(old_num_variables + i), true); Literal b(BooleanVariable(old_num_variables + core.size() + i), true); if (core.size() == 2) { b = Literal(BooleanVariable(old_num_variables + 2), true); if (i == 1) b = b.Negated(); } // a false & b false => previous assumptions (which was false). const Literal old_a = assumptions[index]; ok &= solver->AddTernaryClause(a, b, old_a); // Optional. Also add the two implications a => x and b => x where x is // the negation of the previous assumption variable. ok &= solver->AddBinaryClause(a.Negated(), old_a.Negated()); ok &= solver->AddBinaryClause(b.Negated(), old_a.Negated()); // Optional. Also add the implication a => not(b). ok &= solver->AddBinaryClause(a.Negated(), b.Negated()); // This is the difference with the Fu & Malik algorithm. // If the soft clause protected by old_a has a cost greater than // min_cost then: // - its cost is disminished by min_cost. // - an identical clause with cost min_cost is artifically added to // the problem. CHECK_GE(costs[index], min_cost); if (costs[index] == min_cost) { // The new assumption variable replaces the old one. assumptions[index] = a.Negated(); // Symmetry breaking clauses. for (Literal l : symmetry.ProcessLiteral(index, b)) { ok &= solver->AddBinaryClause(l.Negated(), b.Negated()); } } else { // Since the cost of the given index changes, we need to start a new // "equivalence" class for the symmetry breaking algo and clear the // old one. symmetry.AddInfo(assumptions.size(), b); symmetry.ClearInfo(index); // Reduce the cost of the old assumption. costs[index] -= min_cost; // We add the new assumption with a cost of min_cost. // // Note(user): I think it is nice that these are added after old_a // because assuming old_a will implies all the derived assumptions to // true, and thus they will never appear in a core until old_a is not // an assumption anymore. assumptions.push_back(a.Negated()); costs.push_back(min_cost); reference.push_back(reference[index]); } // For the "<= 1" constraint on the blocking literals. // Note(user): we don't add the ">= 1" side because it is not needed for // the correctness and it doesn't seems to help. at_most_one_constraint.push_back(LiteralWithCoeff(b, 1.0)); // Because we have a core, we know that at least one of the initial // problem variables must be true. This seems to help a bit. // // TODO(user): Experiment more. at_least_one_constraint.push_back(reference[index].Negated()); } // Add the "<= 1" side of the "== 1" constraint. ok &= solver->AddLinearConstraint(false, Coefficient(0), true, Coefficient(1.0), &at_most_one_constraint); // Optional. Add the ">= 1" constraint on the initial problem variables. ok &= solver->AddProblemClause(at_least_one_constraint); if (!ok) { LOG(INFO) << "Unsat while adding a clause."; return SatSolver::MODEL_UNSAT; } } } } SatSolver::Status SolveWithRandomParameters(LogBehavior log, const LinearBooleanProblem& problem, int num_times, SatSolver* solver, std::vector* solution) { Logger logger(log); const SatParameters initial_parameters = solver->parameters(); MTRandom random("A random seed."); SatParameters parameters = initial_parameters; TimeLimit time_limit(parameters.max_time_in_seconds()); // We start with a low conflict limit and increase it until we are able to // solve the problem at least once. After this, the limit stays the same. int max_number_of_conflicts = 5; parameters.set_log_search_progress(false); Coefficient min_seen(std::numeric_limits::max()); Coefficient max_seen(std::numeric_limits::min()); Coefficient best(min_seen); for (int i = 0; i < num_times; ++i) { solver->Backtrack(0); RandomizeDecisionHeuristic(&random, ¶meters); parameters.set_max_number_of_conflicts(max_number_of_conflicts); parameters.set_max_time_in_seconds(time_limit.GetTimeLeft()); parameters.set_random_seed(i); solver->SetParameters(parameters); solver->ResetDecisionHeuristic(); const bool use_obj = random.OneIn(4); if (use_obj) UseObjectiveForSatAssignmentPreference(problem, solver); const SatSolver::Status result = solver->Solve(); if (result == SatSolver::MODEL_UNSAT) { // If the problem is UNSAT after we over-constrained the objective, then // we found an optimal solution, otherwise, even the decision problem is // UNSAT. if (best == kCoefficientMax) return SatSolver::MODEL_UNSAT; return SatSolver::MODEL_SAT; } if (result == SatSolver::LIMIT_REACHED) { // We augment the number of conflict until we have one feasible solution. if (best == kCoefficientMax) ++max_number_of_conflicts; if (time_limit.LimitReached()) return SatSolver::LIMIT_REACHED; continue; } CHECK_EQ(result, SatSolver::MODEL_SAT); std::vector candidate; ExtractAssignment(problem, *solver, &candidate); CHECK(IsAssignmentValid(problem, candidate)); const Coefficient objective = ComputeObjectiveValue(problem, candidate); if (objective < best) { *solution = candidate; best = objective; logger.Log(CnfObjectiveLine(problem, objective)); // Overconstrain the objective. solver->Backtrack(0); if (!AddObjectiveConstraint(problem, false, Coefficient(0), true, objective - 1, solver)) { return SatSolver::MODEL_SAT; } } min_seen = std::min(min_seen, objective); max_seen = std::max(max_seen, objective); logger.Log(StringPrintf("c %lld [%lld, %lld] objective preference: %s %s", objective.value(), min_seen.value(), max_seen.value(), use_obj ? "true" : "false", parameters.ShortDebugString().c_str())); } // Retore the initial parameter (with an updated time limit). parameters = initial_parameters; parameters.set_max_time_in_seconds(time_limit.GetTimeLeft()); solver->SetParameters(parameters); return SatSolver::LIMIT_REACHED; } SatSolver::Status SolveWithLinearScan(LogBehavior log, const LinearBooleanProblem& problem, SatSolver* solver, std::vector* solution) { Logger logger(log); // This has a big positive impact on most problems. UseObjectiveForSatAssignmentPreference(problem, solver); Coefficient objective = kCoefficientMax; if (!solution->empty()) { CHECK(IsAssignmentValid(problem, *solution)); objective = ComputeObjectiveValue(problem, *solution); } while (true) { if (objective != kCoefficientMax) { // Over constrain the objective. solver->Backtrack(0); if (!AddObjectiveConstraint(problem, false, Coefficient(0), true, objective - 1, solver)) { return SatSolver::MODEL_SAT; } } // Solve the problem. const SatSolver::Status result = solver->Solve(); CHECK_NE(result, SatSolver::ASSUMPTIONS_UNSAT); if (result == SatSolver::MODEL_UNSAT) { if (objective == kCoefficientMax) return SatSolver::MODEL_UNSAT; return SatSolver::MODEL_SAT; } if (result == SatSolver::LIMIT_REACHED) { return SatSolver::LIMIT_REACHED; } // Extract the new best solution. CHECK_EQ(result, SatSolver::MODEL_SAT); ExtractAssignment(problem, *solver, solution); CHECK(IsAssignmentValid(problem, *solution)); const Coefficient old_objective = objective; objective = ComputeObjectiveValue(problem, *solution); CHECK_LT(objective, old_objective); logger.Log(CnfObjectiveLine(problem, objective)); } } SatSolver::Status SolveWithCardinalityEncoding( LogBehavior log, const LinearBooleanProblem& problem, SatSolver* solver, std::vector* solution) { Logger logger(log); std::deque repository; // Create one initial node per variables with cost. Coefficient offset(0); std::vector nodes = CreateInitialEncodingNodes(problem.objective(), &offset, &repository); // This algorithm only work with weights of the same magnitude. CHECK(!nodes.empty()); const Coefficient reference = nodes.front()->weight(); for (const EncodingNode* n : nodes) CHECK_EQ(n->weight(), reference); // Initialize the current objective. Coefficient objective = kCoefficientMax; Coefficient upper_bound = kCoefficientMax; if (!solution->empty()) { CHECK(IsAssignmentValid(problem, *solution)); objective = ComputeObjectiveValue(problem, *solution); upper_bound = objective + offset; } // Print the number of variables with a non-zero cost. logger.Log(StringPrintf("c #weights:%zu #vars:%d #constraints:%d", nodes.size(), problem.num_variables(), problem.constraints_size())); // Create the sorter network. solver->Backtrack(0); EncodingNode* root = MergeAllNodesWithDeque(upper_bound, nodes, solver, &repository); logger.Log(StringPrintf("c encoding depth:%d", root->depth())); while (true) { if (objective != kCoefficientMax) { // Over constrain the objective by fixing the variable index - 1 of the // root node to 0. const int index = offset.value() + objective.value(); if (index == 0) return SatSolver::MODEL_SAT; solver->Backtrack(0); if (!solver->AddUnitClause(root->literal(index - 1).Negated())) { return SatSolver::MODEL_SAT; } } // Solve the problem. const SatSolver::Status result = solver->Solve(); CHECK_NE(result, SatSolver::ASSUMPTIONS_UNSAT); if (result == SatSolver::MODEL_UNSAT) { if (objective == kCoefficientMax) return SatSolver::MODEL_UNSAT; return SatSolver::MODEL_SAT; } if (result == SatSolver::LIMIT_REACHED) return SatSolver::LIMIT_REACHED; // Extract the new best solution. CHECK_EQ(result, SatSolver::MODEL_SAT); ExtractAssignment(problem, *solver, solution); CHECK(IsAssignmentValid(problem, *solution)); const Coefficient old_objective = objective; objective = ComputeObjectiveValue(problem, *solution); CHECK_LT(objective, old_objective); logger.Log(CnfObjectiveLine(problem, objective)); } } 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(); } bool EmptyEncodingNode(const EncodingNode* a) { return a->size() == 0; } } // namespace SatSolver::Status SolveWithCardinalityEncodingAndCore( LogBehavior log, const LinearBooleanProblem& problem, SatSolver* solver, std::vector* solution) { Logger logger(log); SatParameters parameters = solver->parameters(); std::deque repository; // Create one initial nodes per variables with cost. Coefficient offset(0); std::vector nodes = CreateInitialEncodingNodes(problem.objective(), &offset, &repository); // Initialize the bounds. // This is in term of number of variables not at their minimal value. Coefficient lower_bound(0); Coefficient upper_bound(kint64max); if (!solution->empty()) { CHECK(IsAssignmentValid(problem, *solution)); upper_bound = ComputeObjectiveValue(problem, *solution) + offset; } // Print the number of variables with a non-zero cost. logger.Log(StringPrintf("c #weights:%zu #vars:%d #constraints:%d", nodes.size(), problem.num_variables(), problem.constraints_size())); // This is used by the "stratified" approach. Coefficient stratified_lower_bound(0); if (parameters.max_sat_stratification() == SatParameters::STRATIFICATION_DESCENT) { // In this case, we initialize it to the maximum assumption weights. for (EncodingNode* n : nodes) { stratified_lower_bound = std::max(stratified_lower_bound, n->weight()); } } // Start the algorithm. int max_depth = 0; std::string previous_core_info = ""; for (int iter = 0;; ++iter) { // 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) * n->weight(); } // Fix the nodes right-most variables that are above the gap. if (upper_bound != kCoefficientMax) { const Coefficient gap = upper_bound - lower_bound; if (gap == 0) return SatSolver::MODEL_SAT; for (EncodingNode* n : nodes) { n->ApplyUpperBound((gap / n->weight()).value(), solver); } } // Remove the empty nodes. nodes.erase(std::remove_if(nodes.begin(), nodes.end(), EmptyEncodingNode), nodes.end()); // Sort the nodes. switch (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 (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 iterations... std::reverse(nodes.begin(), nodes.end()); } // Extract the assumptions from the nodes. std::vector assumptions; for (EncodingNode* n : nodes) { if (n->weight() >= stratified_lower_bound) { assumptions.push_back(n->literal(0).Negated()); } } // Display the progress. const std::string gap_string = (upper_bound == kCoefficientMax) ? "" : StringPrintf(" gap:%lld", (upper_bound - lower_bound).value()); logger.Log( StringPrintf("c iter:%d [%s] lb:%lld%s assumptions:%zu depth:%d", iter, previous_core_info.c_str(), lower_bound.value() - offset.value() + static_cast(problem.objective().offset()), gap_string.c_str(), nodes.size(), max_depth)); // Solve under the assumptions. const SatSolver::Status result = solver->ResetAndSolveWithGivenAssumptions(assumptions); if (result == SatSolver::MODEL_SAT) { // Extract the new solution and save it if it is the best found so far. std::vector temp_solution; ExtractAssignment(problem, *solver, &temp_solution); CHECK(IsAssignmentValid(problem, temp_solution)); const Coefficient obj = ComputeObjectiveValue(problem, temp_solution); if (obj + offset < upper_bound) { *solution = temp_solution; logger.Log(CnfObjectiveLine(problem, obj)); upper_bound = obj + offset; } // If not all assumptions where taken, continue with a lower stratified // bound. Otherwise we have an optimal solution. const Coefficient old_lower_bound = stratified_lower_bound; for (EncodingNode* n : nodes) { if (n->weight() < old_lower_bound) { if (stratified_lower_bound == old_lower_bound || n->weight() > stratified_lower_bound) { stratified_lower_bound = n->weight(); } } } if (stratified_lower_bound < old_lower_bound) continue; return SatSolver::MODEL_SAT; } if (result != SatSolver::ASSUMPTIONS_UNSAT) return result; // We have a new core. std::vector core = solver->GetLastIncompatibleDecisions(); if (parameters.minimize_core()) MinimizeCore(solver, &core); // Compute the min weight of all the nodes in the core. // The lower bound will be increased by that much. Coefficient min_weight = kCoefficientMax; { int index = 0; for (int i = 0; i < core.size(); ++i) { for (; index < nodes.size() && nodes[index]->literal(0).Negated() != core[i]; ++index) { } CHECK_LT(index, nodes.size()); min_weight = std::min(min_weight, nodes[index]->weight()); } } previous_core_info = StringPrintf("core:%zu mw:%lld", core.size(), min_weight.value()); // Increase stratified_lower_bound according to the parameters. if (stratified_lower_bound < min_weight && parameters.max_sat_stratification() == SatParameters::STRATIFICATION_ASCENT) { stratified_lower_bound = min_weight; } // Backtrack to be able to add new constraints. solver->Backtrack(0); int new_node_index = 0; if (core.size() == 1) { // The core will be reduced at the beginning of the next loop. // Find the associated node, and call IncreaseNodeSize() on it. CHECK(solver->Assignment().LiteralIsFalse(core[0])); for (EncodingNode* n : nodes) { if (n->literal(0).Negated() == core[0]) { IncreaseNodeSize(n, solver); break; } } } else { // Remove from nodes the EncodingNode in the core, merge them, and add the // resulting EncodingNode at the back. int index = 0; std::vector to_merge; 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]->literal(0).Negated() != core[i]; ++index) { CHECK_LT(index, nodes.size()); nodes[new_node_index] = nodes[index]; ++new_node_index; } CHECK_LT(index, nodes.size()); to_merge.push_back(nodes[index]); // 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 (nodes[index]->weight() > min_weight) { nodes[index]->set_weight(nodes[index]->weight() - min_weight); nodes[new_node_index] = nodes[index]; ++new_node_index; } ++index; } for (; index < nodes.size(); ++index) { nodes[new_node_index] = nodes[index]; ++new_node_index; } nodes.resize(new_node_index); nodes.push_back(LazyMergeAllNodeWithPQ(to_merge, solver, &repository)); IncreaseNodeSize(nodes.back(), solver); max_depth = std::max(max_depth, nodes.back()->depth()); nodes.back()->set_weight(min_weight); CHECK(solver->AddUnitClause(nodes.back()->literal(0))); } } } SatSolver::Status MinimizeIntegerVariableWithLinearScan( IntegerVariable objective_var, const std::function& feasible_solution_observer, Model* model) { // Timing. WallTimer wall_timer; UserTimer user_timer; wall_timer.Start(); user_timer.Start(); SatSolver* sat_solver = model->GetOrCreate(); IntegerTrail* integer_trail = model->GetOrCreate(); LOG(INFO) << "#Boolean_variables:" << sat_solver->NumVariables(); // Simple linear scan algorithm to find the optimal. SatSolver::Status result; bool model_is_feasible = false; int objective; while (true) { result = sat_solver->Solve(); if (result != SatSolver::MODEL_SAT) break; // The objective is the current lower bound of the objective_var. objective = integer_trail->LowerBound(objective_var); // We have a solution! model_is_feasible = true; feasible_solution_observer(*model); // Restrict the objective. sat_solver->Backtrack(0); integer_trail->Enqueue( IntegerLiteral::LowerOrEqual(objective_var, objective - 1), {}, {}); } // Display summary. if (model_is_feasible) { printf("objective: %d\n", objective); } else { printf("objective: NA\n"); } printf("status: %s\n", result == SatSolver::MODEL_UNSAT ? "OPTIMAL" : "LIMIT_REACHED"); printf("conflicts: %lld\n", sat_solver->num_failures()); printf("branches: %lld\n", sat_solver->num_branches()); printf("propagations: %lld\n", sat_solver->num_propagations()); printf("walltime: %f\n", wall_timer.Get()); printf("usertime: %f\n", user_timer.Get()); printf("deterministic_time: %f\n", sat_solver->deterministic_time()); return result; } } // namespace sat } // namespace operations_research