Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
|
#include <clause.h>
Public Member Functions | |
BinaryImplicationGraph (Model *model) | |
BinaryImplicationGraph (const BinaryImplicationGraph &)=delete | |
This type is neither copyable nor movable. | |
BinaryImplicationGraph & | operator= (const BinaryImplicationGraph &)=delete |
~BinaryImplicationGraph () override | |
bool | Propagate (Trail *trail) final |
SatPropagator interface. | |
absl::Span< const Literal > | Reason (const Trail &trail, int trail_index, int64_t conflict_id) const final |
void | Resize (int num_variables) |
Resizes the data structure. | |
bool | IsEmpty () const final |
Returns true if there is no constraints in this class. | |
bool | AddBinaryClause (Literal a, Literal b) |
bool | AddImplication (Literal a, Literal b) |
void | EnableSharing (bool enable) |
void | SetAdditionCallback (std::function< void(Literal, Literal)> f) |
ABSL_MUST_USE_RESULT bool | AddAtMostOne (absl::Span< const Literal > at_most_one) |
void | MinimizeConflictWithReachability (std::vector< Literal > *c) |
void | MinimizeConflictExperimental (const Trail &trail, std::vector< Literal > *c) |
void | MinimizeConflictFirst (const Trail &trail, std::vector< Literal > *c, SparseBitset< BooleanVariable > *marked) |
void | MinimizeConflictFirstWithTransitiveReduction (const Trail &trail, std::vector< Literal > *c, absl::BitGenRef random) |
void | RemoveFixedVariables () |
bool | DetectEquivalences (bool log_info=false) |
bool | IsDag () const |
const std::vector< LiteralIndex > & | ReverseTopologicalOrder () const |
const absl::InlinedVector< Literal, 6 > & | Implications (Literal l) const |
Literal | RepresentativeOf (Literal l) const |
bool | ComputeTransitiveReduction (bool log_info=false) |
bool | TransformIntoMaxCliques (std::vector< std::vector< Literal > > *at_most_ones, int64_t max_num_explored_nodes=1e8) |
const std::vector< std::vector< Literal > > & | GenerateAtMostOnesWithLargeWeight (const std::vector< Literal > &literals, const std::vector< double > &lp_values) |
std::vector< absl::Span< const Literal > > | HeuristicAmoPartition (std::vector< Literal > *literals) |
int64_t | num_propagations () const |
Number of literal propagated by this class (including conflicts). | |
int64_t | num_inspections () const |
Number of literals inspected by this class during propagation. | |
int64_t | num_minimization () const |
MinimizeClause() stats. | |
int64_t | num_literals_removed () const |
bool | IsRedundant (Literal l) const |
int64_t | num_redundant_literals () const |
int64_t | num_redundant_implications () const |
Number of implications removed by transitive reduction. | |
int64_t | num_implications () const |
int64_t | literal_size () const |
template<typename Output > | |
void | ExtractAllBinaryClauses (Output *out) const |
void | SetDratProofHandler (DratProofHandler *drat_proof_handler) |
void | ChangeReason (int trail_index, Literal new_reason) |
const std::vector< Literal > & | DirectImplications (Literal literal) |
LiteralIndex | RandomImpliedLiteral (Literal lhs) |
int | DirectImplicationsEstimatedSize (Literal literal) const |
bool | FindFailedLiteralAroundVar (BooleanVariable var, bool *is_unsat) |
int64_t | NumImplicationOnVariableRemoval (BooleanVariable var) |
void | RemoveBooleanVariable (BooleanVariable var, std::deque< std::vector< Literal > > *postsolve_clauses) |
For all possible a => var => b, add a => b. | |
bool | IsRemoved (Literal l) const |
void | RemoveAllRedundantVariables (std::deque< std::vector< Literal > > *postsolve_clauses) |
void | CleanupAllRemovedAndFixedVariables () |
void | ResetWorkDone () |
int64_t | WorkDone () const |
template<bool use_weight = true> | |
std::vector< Literal > | ExpandAtMostOneWithWeight (absl::Span< const Literal > at_most_one, const util_intops::StrongVector< LiteralIndex, bool > &can_be_included, const util_intops::StrongVector< LiteralIndex, double > &expanded_lp_values) |
Same as ExpandAtMostOne() but try to maximize the weight in the clique. | |
void | ResetAtMostOneIterator () |
Restarts the at_most_one iterator. | |
absl::Span< const Literal > | NextAtMostOne () |
Returns the next at_most_one, or a span of size 0 when finished. | |
void | RemoveDuplicates () |
Clean up implications list that might have duplicates. | |
Public Member Functions inherited from operations_research::sat::SatPropagator | |
SatPropagator (const std::string &name) | |
SatPropagator (const SatPropagator &)=delete | |
This type is neither copyable nor movable. | |
SatPropagator & | operator= (const SatPropagator &)=delete |
virtual | ~SatPropagator ()=default |
void | SetPropagatorId (int id) |
Sets/Gets this propagator unique id. | |
int | PropagatorId () const |
virtual void | Untrail (const Trail &, int trail_index) |
bool | PropagatePreconditionsAreSatisfied (const Trail &trail) const |
######################## Implementations below ######################## | |
bool | PropagationIsDone (const Trail &trail) const |
Returns true iff all the trail was inspected by this propagator. | |
Additional Inherited Members | |
Protected Attributes inherited from operations_research::sat::SatPropagator | |
const std::string | name_ |
int | propagator_id_ |
int | propagation_trail_index_ |
Special class to store and propagate clauses of size 2 (i.e. implication). Such clauses are never deleted. Together, they represent the 2-SAT part of the problem. Note that 2-SAT satisfiability is a polynomial problem, but W2SAT (weighted 2-SAT) is NP-complete.
Note(user): All the variables in a strongly connected component are equivalent and can be thus merged as one. This is relatively cheap to compute from time to time (linear complexity). We will also get contradiction (a <=> not a) this way. This is done by DetectEquivalences().
Note(user): An implication (a => not a) implies that a is false. I am not sure it is worth detecting that because if the solver assign a to true, it will learn that right away. I don't think we can do it faster.
Note(user): The implication graph can be pruned. This is called the transitive reduction of a graph. For instance If a => {b,c} and b => {c}, then there is no need to store a => {c}. The transitive reduction is unique on an acyclic graph. Computing it will allow for a faster propagation and memory reduction. It is however not cheap. Maybe simple lazy heuristics to remove redundant arcs are better. Note that all the learned clauses we add will never be redundant (but they could introduce cycles). This is done by ComputeTransitiveReduction().
Note(user): This class natively support at most one constraints. This is a way to reduced significantly the memory and size of some 2-SAT instances. However, it is not fully exploited for pure SAT problems. See TransformIntoMaxCliques().
Note(user): Add a preprocessor to remove duplicates in the implication lists.
References for most of the above and more:
|
inlineexplicit |
|
delete |
This type is neither copyable nor movable.
|
inlineoverride |
bool operations_research::sat::BinaryImplicationGraph::AddAtMostOne | ( | absl::Span< const Literal > | at_most_one | ) |
An at most one constraint of size n is a compact way to encode n * (n - 1) implications. This must only be called at level zero.
Returns false if this creates a conflict. Currently this can only happens if there is duplicate literal already assigned to true in this constraint.
If the final AMO size is smaller than at_most_one_expansion_size parameters, we fully expand it.
Temporarily copy the at_most_one constraint at the end of at_most_one_buffer_. It will be cleaned up and added by CleanUpAndAddAtMostOnes().
Adds the binary clause (a OR b), which is the same as (not a => b).
Preconditions:
If we are at a positive decision level, we will propagate something if we can. However, if both literal are false, we will just return false and do nothing. In all other case, we will return true.
Tricky: If this is the first clause, the propagator will be added and assumed to be in a "propagated" state. This makes sure this is the case.
|
inline |
void operations_research::sat::BinaryImplicationGraph::CleanupAllRemovedAndFixedVariables | ( | ) |
bool operations_research::sat::BinaryImplicationGraph::ComputeTransitiveReduction | ( | bool | log_info = false | ) |
Prunes the implication graph by calling first DetectEquivalences() to remove cycle and then by computing the transitive reduction of the remaining DAG.
Returns false if the model is detected to be UNSAT (this needs to call DetectEquivalences() if not already done).
For each node we do a graph traversal and only keep the literals at maximum distance 1. This only works because we have a DAG when ignoring the "redundant" literal marked by DetectEquivalences(). Note that we also need no duplicates in the implications list for correctness which is also guaranteed by DetectEquivalences().
In most situation reverse_topological_order_ contains no redundant, fixed or removed variables. But the reverse_topological_order_ is only recomputed when new binary are added to the graph, not when new variable are fixed.
This is a "poor" version of the tree look stuff, but it does show good improvement. If we just processed one of the child of root, we don't need to re-explore it.
This is a corner case where because of equivalent literal, root appear in implications_[root], we will remove it below.
When this happens, then root must be false, we handle this just after the loop.
We have a DAG, so direct_child could only be marked first.
Also mark all the ones reachable through the root AMOs.
Failed literal probing. If both x and not(x) are marked then root must be false. Note that because we process "roots" in reverse topological order, we will fix the LCA of x and not(x) first.
We tested that at the beginning.
We propagate right away the binary implications so that we do not need to consider all antecedents of root in the transitive reduction.
Only keep the non-marked literal (and the redundant one which are never marked). We mark root to remove it in the corner case where it was there.
Abort if the computation involved is too big.
If we aborted early, we might no longer have both a=>b and not(b)=>not(a). This is not desirable has some algo relies on this invariant. We fix this here.
bool operations_research::sat::BinaryImplicationGraph::DetectEquivalences | ( | bool | log_info = false | ) |
Returns false if the model is unsat, otherwise detects equivalent variable (with respect to the implications only) and reorganize the propagation lists accordingly.
This was already called, and no new constraint where added. Note that new fixed variable cannot create new equivalence, only new binary clauses do.
Lets remove all fixed variables first.
The old values will still be valid.
If one is fixed then all must be fixed. Note that the reason why the propagation didn't already do that and we don't always get fixed component of size 1 is because of the potential newly fixed literals above.
In any case, all fixed literals are marked as redundant.
Next component.
We ignore variable that appear in no constraints.
We always take the smallest literal index (which also corresponds to the smallest BooleanVariable index) as a representative. This make sure that the representative of a literal l and the one of not(l) will be the negation of each other. There is also reason to think that it is heuristically better to use a BooleanVariable that was created first.
Sets the representative.
Detect if x <=> not(x) which means unsat. Note that we relly on the fact that when sorted, they will both be consecutive in the list.
Merge all the lists in implications_[representative].
Add representative <=> literal.
Remark: this relation do not need to be added to a DRAT proof since the redundant variables should never be used again for a pure SAT problem.
Remap all at most ones. Remove fixed variables, process duplicates. Note that this might result in more implications when we expand small at most one.
const std::vector< Literal > & operations_research::sat::BinaryImplicationGraph::DirectImplications | ( | Literal | literal | ) |
The literals that are "directly" implied when literal is set to true. This is not a full "reachability". It includes at most ones propagation. The set of all direct implications is enough to describe the implications graph completely.
When doing blocked clause elimination of bounded variable elimination, one only need to consider this list and not the full reachability.
(user): lazy cleanup the lists on is_removed_?
(user): Mark fixed variable as is_removed_ for faster iteration?
Clear old state.
Fill new state.
|
inline |
A proxy for DirectImplications().size(), However we currently do not maintain it perfectly. It is exact each time DirectImplications() is called, and we update it in some situation but we don't deal with fixed variables, at_most ones and duplicates implications for now.
|
inline |
When set, the callback will be called on ALL newly added binary clauses.
The EnableSharing() function can be used to disable sharing temporarily for the clauses that are imported from the Shared repository already.
template std::vector< Literal > operations_research::sat::BinaryImplicationGraph::ExpandAtMostOneWithWeight< false > | ( | absl::Span< const Literal > | at_most_one, |
const util_intops::StrongVector< LiteralIndex, bool > & | can_be_included, | ||
const util_intops::StrongVector< LiteralIndex, double > & | expanded_lp_values ) |
Same as ExpandAtMostOne() but try to maximize the weight in the clique.
Make sure both version are compiled.
Do not spend too much time here.
We can't generate a violated cut this way. This is because intersection contains all the possible ways to extend the current clique.
Expand? The negation of any literal in the intersection is a valid way to extend the clique.
Heuristic: use literal with largest lp value. We randomize slightly.
If we don't use weight, we prefer variable that comes first.
|
inline |
Extract all the binary clauses managed by this class. The Output type must support an AddBinaryClause(Literal a, Literal b) function.
Important: This currently does NOT include at most one constraints.
Note(user): We almost always have both a => b and not(b) => not(a) in our implications_ database. Except if ComputeTransitiveReduction() was aborted early, but in this case, if only one is present, the other could be removed, so we shouldn't need to output it.
bool operations_research::sat::BinaryImplicationGraph::FindFailedLiteralAroundVar | ( | BooleanVariable | var, |
bool * | is_unsat ) |
Variable elimination by replacing everything of the form a => var => b by a => b. We ignore any a => a so the number of new implications is not always just the product of the two direct implication list of var and not(var). However, if a => var => a, then a and var are equivalent, so this case will be removed if one run DetectEquivalences() before this. Similarly, if a => var => not(a) then a must be false and this is detected and dealt with by FindFailedLiteralAroundVar().
not(l) => literal => l.
const std::vector< std::vector< Literal > > & operations_research::sat::BinaryImplicationGraph::GenerateAtMostOnesWithLargeWeight | ( | const std::vector< Literal > & | literals, |
const std::vector< double > & | lp_values ) |
LP clique cut heuristic. Returns a set of "at most one" constraints on the given literals or their negation that are violated by the current LP solution. Note that this assumes that lp_value(lit) = 1 - lp_value(lit.Negated()).
The literal and lp_values vector are in one to one correspondence. We will only generate clique with these literals or their negation.
We only want to generate a cut with literals from the LP, not extra ones.
We want highest sum first.
First heuristic. Currently we only consider violated at most one of size 2, and extend them. Right now, the code is a bit slow to try too many at every LP node so it is why we are defensive like this. Note also that because we currently still statically add the initial implications, this will only add cut based on newly learned binary clause. Or the one that were not added to the relaxation in the first place.
We consider only one candidate for each current_literal.
Do not genate too many cut at once.
Expand to a maximal at most one each candidates before returning them.
std::vector< absl::Span< const Literal > > operations_research::sat::BinaryImplicationGraph::HeuristicAmoPartition | ( | std::vector< Literal > * | literals | ) |
Heuristically identify "at most one" between the given literals, swap them around and return these amo as span inside the literals vector.
(user): Use deterministic limit.
(user): Explore the graph instead of just looking at full amo, especially since we expand small ones.
Priority queue of (intersection_size, start_of_amo).
Compute for each at most one that might overlap, its overlaping size and stores its start in the at_most_one_buffer_.
This is in O(num_literal in amo).
Abort early if we are done.
Consume AMOs, update size.
Recompute size.
re-add with new size.
Mark the literal of that at most one at false.
Extract the intersection by moving it in [num_processed, num_processed + intersection_size).
|
inline |
Returns true if DetectEquivalences() has been called and no new binary clauses have been added since then. When this is true then there is no cycle in the binary implication graph (modulo the redundant literals that form a cycle with their representative).
|
inlinefinalvirtual |
Returns true if there is no constraints in this class.
Reimplemented from operations_research::sat::SatPropagator.
|
inline |
Returns true if this literal is fixed or is equivalent to another literal. This means that it can just be ignored in most situation.
|
inline |
|
inline |
void operations_research::sat::BinaryImplicationGraph::MinimizeConflictExperimental | ( | const Trail & | trail, |
std::vector< Literal > * | conflict ) |
Identify and remove the redundant literals from the given conflict. 1/ If a -> b then a can be removed from the conflict clause. This is because not b -> not a. 2/ a -> b can only happen if level(a) <= level(b). 3/ Because of 2/, cycles can appear only at the same level. The vector is_simplified_ is used to avoid removing all elements of a cycle. Note that this is not optimal in the sense that we may not remove a literal that can be removed.
void operations_research::sat::BinaryImplicationGraph::MinimizeConflictFirst | ( | const Trail & | trail, |
std::vector< Literal > * | conflict, | ||
SparseBitset< BooleanVariable > * | marked ) |
Same as MinimizeConflictWithReachability() but also mark (in the given SparseBitset) the reachable literal already assigned to false. These literals will be implied if the 1-UIP literal is assigned to false, and the classic minimization algorithm can take advantage of that.
void operations_research::sat::BinaryImplicationGraph::MinimizeConflictFirstWithTransitiveReduction | ( | const Trail & | trail, |
std::vector< Literal > * | conflict, | ||
absl::BitGenRef | random ) |
Same as MinimizeConflictFirst() but take advantage of this reachability computation to remove redundant implication in the implication list of the first UIP conflict.
The randomization allow to find more redundant implication since to find a => b and remove b, a must be before b in direct_implications. Note that a std::reverse() could work too. But randomization seems to work better. Probably because it has other impact on the search tree.
The literal is already marked! so it must be implied by one of the previous literal in the direct_implications list. We can safely remove it.
void operations_research::sat::BinaryImplicationGraph::MinimizeConflictWithReachability | ( | std::vector< Literal > * | conflict | ) |
Uses the binary implication graph to minimize the given conflict by removing literals that implies others. The idea is that if a and b are two literals from the given conflict and a => b (which is the same as not(b) => not(a)) then a is redundant and can be removed.
Here, we remove all the literal whose negation are implied by the negation of the 1-UIP literal (which always appear first in the given conflict). Note that this algorithm is "optimal" in the sense that it leads to a minimized conflict with a backjump level as low as possible. However, not all possible literals are removed.
Compute the reachability from the literal "not(conflict->front())" using an iterative dfs.
The idea here is that since we already compute the reachability from the root literal, we can use this computation to remove any implication root_literal => b if there is already root_literal => a and b is reachable from a.
We treat the direct implications differently so we can also remove the redundant implications from this list at the same time.
The "trick" is to unmark 'l'. This way, if we explore it twice, it means that this l is reachable from some other 'l' from the direct implication list. Remarks:
Now we can prune the direct implications list and make sure are the literals there are marked.
absl::Span< const Literal > operations_research::sat::BinaryImplicationGraph::NextAtMostOne | ( | ) |
|
inline |
|
inline |
|
inline |
|
inline |
|
inline |
|
inline |
|
inline |
int64_t operations_research::sat::BinaryImplicationGraph::NumImplicationOnVariableRemoval | ( | BooleanVariable | var | ) |
We should have dealt with that in FindFailedLiteralAroundVar().
l => literal => l: equivalent variable!
|
delete |
|
finalvirtual |
SatPropagator interface.
Implements operations_research::sat::SatPropagator.
LiteralIndex operations_research::sat::BinaryImplicationGraph::RandomImpliedLiteral | ( | Literal | lhs | ) |
Returns a random literal in DirectImplications(lhs). Note that this is biased if lhs appear in some most one, but it is constant time, which is a lot faster than computing DirectImplications() and then sampling from it.
We are unlucky and just picked the wrong literal: take a different one.
|
finalvirtual |
Explains why the literal at given trail_index was propagated by returning a reason for this propagation. This will only be called for literals that are on the trail and were propagated by this class.
The interpretation is that because all the literals of a reason were assigned to false, we could deduce the assignment of the given variable.
The returned Span has to be valid until the literal is untrailed. A client can use trail_.GetEmptyVectorToStoreReason() if it doesn't have a memory location that already contains the reason.
If conlict id is positive, then this is called during first UIP resolution and we will backtrack over this literal right away, so we don't need to have a span that survive more than once.
Reimplemented from operations_research::sat::SatPropagator.
void operations_research::sat::BinaryImplicationGraph::RemoveAllRedundantVariables | ( | std::deque< std::vector< Literal > > * | postsolve_clauses | ) |
void operations_research::sat::BinaryImplicationGraph::RemoveBooleanVariable | ( | BooleanVariable | var, |
std::deque< std::vector< Literal > > * | postsolve_clauses ) |
For all possible a => var => b, add a => b.
Notify the deletion to the proof checker and the postsolve.
We need to remove any occurrence of var in our implication lists, this will be delayed to the CleanupAllRemovedVariables() call.
void operations_research::sat::BinaryImplicationGraph::RemoveDuplicates | ( | ) |
void operations_research::sat::BinaryImplicationGraph::RemoveFixedVariables | ( | ) |
This must only be called at decision level 0 after all the possible propagations. It:
Nothing to do if nothing changed since last call.
The code assumes that everything is already propagated. Otherwise we will remove implications that didn't propagate yet!
If b is true and a -> b then because not b -> not a, all the implications list that contains b will be marked by this process. And the ones that contains not(b) should correspond to a false literal!
We mark its representative instead.
|
inline |
Returns the representative of the equivalence class of l (or l itself if it is on its own). Note that DetectEquivalences() should have been called to get any non-trival results.
|
inline |
|
inline |
ExpandAtMostOneWithWeight() will increase this, so a client can put a limit on this possibly expansive operation.
void operations_research::sat::BinaryImplicationGraph::Resize | ( | int | num_variables | ) |
Resizes the data structure.
--— BinaryImplicationGraph --—
|
inline |
One must call DetectEquivalences() first, this is CHECKed. Returns a list so that if x => y, then x is after y.
|
inline |
bool operations_research::sat::BinaryImplicationGraph::TransformIntoMaxCliques | ( | std::vector< std::vector< Literal > > * | at_most_ones, |
int64_t | max_num_explored_nodes = 1e8 ) |
Another way of representing an implication graph is a list of maximal "at most one" constraints, each forming a max-clique in the incompatibility graph. This representation is useful for having a good linear relaxation.
This function will transform each of the given constraint into a maximal one in the underlying implication graph. Constraints that are redundant after other have been expanded (i.e. included into) will be cleared.
Returns false if the model is detected to be UNSAT (this needs to call DetectEquivalences() if not already done).
The code below assumes a DAG.
Data to detect inclusion of base amo into extend amo.
We starts by processing larger constraints first. But we want the output order to be stable.
Note(user): Because we always use literal with the smallest variable indices as representative, this make sure that if possible, we express the clique in term of user provided variable (that are always created first).
Remap the clique to only use representative.
We skip anything that can be presolved further as the code below do not handle duplicate well.
Special case for clique of size 2, we don't expand them if they are included in an already added clique.
Save the non-extended version as possible subset.
We only expand the clique as long as we didn't spend too much time.
Save the extended version as possible superset.
Also index clique for size 2 quick lookup.
Remove clique (before extension) that are included in an extended one.
Abort if one was already deleted.
If an extended clique already cover a deleted one, we cannot try to remove it by looking at its non-extended version.
|
inline |