// Copyright 2010-2018 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. // // An implementation of a cost-scaling push-relabel algorithm for the // assignment problem (minimum-cost perfect bipartite matching), from // the paper of Goldberg and Kennedy (1995). // // // This implementation finds the minimum-cost perfect assignment in // the given graph with integral edge weights set through the // SetArcCost method. // // The running time is O(n*m*log(nC)) where n is the number of nodes, // m is the number of edges, and C is the largest magnitude of an edge cost. // In principle it can be worse than the Hungarian algorithm but we don't know // of any class of problems where that actually happens. An additional sqrt(n) // factor could be shaved off the running time bound using the technique // described in http://dx.doi.org/10.1137/S0895480194281185 // (see also http://theory.stanford.edu/~robert/papers/glob_upd.ps). // // // Example usage: // // #include "ortools/graph/graph.h" // #include "ortools/graph/linear_assignment.h" // // // Choose a graph implementation (we recommend StaticGraph<>). // typedef util::StaticGraph<> Graph; // // // Define a num_nodes / 2 by num_nodes / 2 assignment problem: // const int num_nodes = ... // const int num_arcs = ... // const int num_left_nodes = num_nodes / 2; // Graph graph(num_nodes, num_arcs); // std::vector arc_costs(num_arcs); // for (int arc = 0; arc < num_arcs; ++arc) { // const int arc_tail = ... // must be in [0, num_left_nodes) // const int arc_head = ... // must be in [num_left_nodes, num_nodes) // graph.AddArc(arc_tail, arc_head); // arc_costs[arc] = ... // } // // // Build the StaticGraph. You can skip this step by using a ListGraph<> // // instead, but then the ComputeAssignment() below will be slower. It is // // okay if your graph is small and performance is not critical though. // { // std::vector arc_permutation; // graph.Build(&arc_permutation); // util::Permute(arc_permutation, &arc_costs); // } // // // Construct the LinearSumAssignment. // ::operations_research::LinearSumAssignment a(graph, num_left_nodes); // for (int arc = 0; arc < num_arcs; ++arc) { // // You can also replace 'arc_costs[arc]' by something like // // ComputeArcCost(permutation.empty() ? arc : permutation[arc]) // // if you don't want to store the costs in arc_costs to save memory. // a.SetArcCost(arc, arc_costs[arc]); // } // // // Compute the optimum assignment. // bool success = a.ComputeAssignment(); // // Retrieve the cost of the optimum assignment. // operations_research::CostValue optimum_cost = a.GetCost(); // // Retrieve the node-node correspondence of the optimum assignment and the // // cost of each node pairing. // for (int left_node = 0; left_node < num_left_nodes; ++left_node) { // const int right_node = a.GetMate(left_node); // operations_research::CostValue node_pair_cost = // a.GetAssignmentCost(left_node); // ... // } // // In the following, we consider a bipartite graph // G = (V = X union Y, E subset XxY), // where V denotes the set of nodes (vertices) in the graph, E denotes // the set of arcs (edges), n = |V| denotes the number of nodes in the // graph, and m = |E| denotes the number of arcs in the graph. // // The set of nodes is divided into two parts, X and Y, and every arc // must go between a node of X and a node of Y. With each arc is // associated a cost c(v, w). A matching M is a subset of E with the // property that no two arcs in M have a head or tail node in common, // and a perfect matching is a matching that touches every node in the // graph. The cost of a matching M is the sum of the costs of all the // arcs in M. // // The assignment problem is to find a perfect matching of minimum // cost in the given bipartite graph. The present algorithm reduces // the assignment problem to an instance of the minimum-cost flow // problem and takes advantage of special properties of the resulting // minimum-cost flow problem to solve it efficiently using a // push-relabel method. For more information about minimum-cost flow // see google3/ortools/graph/min_cost_flow.h // // The method used here is the cost-scaling approach for the // minimum-cost circulation problem as described in [Goldberg and // Tarjan] with some technical modifications: // 1. For efficiency, we solve a transportation problem instead of // minimum-cost circulation. We might revisit this decision if it // is important to handle problems in which no perfect matching // exists. // 2. We use a modified "asymmetric" notion of epsilon-optimality in // which left-to-right residual arcs are required to have reduced // cost bounded below by zero and right-to-left residual arcs are // required to have reduced cost bounded below by -epsilon. For // each residual arc direction, the reduced-cost threshold for // admissibility is epsilon/2 above the threshold for epsilon // optimality. // 3. We do not limit the applicability of the relabeling operation to // nodes with excess. Instead we use the double-push operation // (discussed in the Goldberg and Kennedy CSA paper and Kennedy's // thesis) which relabels right-side nodes just *after* they have // been discharged. // The above differences are explained in detail in [Kennedy's thesis] // and explained not quite as cleanly in [Goldberg and Kennedy's CSA // paper]. But note that the thesis explanation uses a value of // epsilon that's double what we use here. // // Some definitions: // Active: A node is called active when it has excess. It is // eligible to be pushed from. In this implementation, every active // node is on the left side of the graph where prices are determined // implicitly, so no left-side relabeling is necessary before // pushing from an active node. We do, however, need to compute // the implications for price changes on the affected right-side // nodes. // Admissible: A residual arc (one that can carry more flow) is // called admissible when its reduced cost is small enough. We can // push additional flow along such an arc without violating // epsilon-optimality. In the case of a left-to-right residual // arc, the reduced cost must be at most epsilon/2. In the case of // a right-to-left residual arc, the reduced cost must be at // most -epsilon/2. The careful reader will note that these thresholds // are not used explicitly anywhere in this implementation, and // the reason is the implicit pricing of left-side nodes. // Reduced cost: Essentially an arc's reduced cost is its // complementary slackness. In push-relabel algorithms this is // c_p(v, w) = p(v) + c(v, w) - p(w), // where p() is the node price function and c(v, w) is the cost of // the arc from v to w. See min_cost_flow.h for more details. // Partial reduced cost: We maintain prices implicitly for left-side // nodes in this implementation, so instead of reduced costs we // work with partial reduced costs, defined as // c'_p(v, w) = c(v, w) - p(w). // // We check at initialization time for the possibility of arithmetic // overflow and warn if the given costs are too large. In many cases // the bound we use to trigger the warning is pessimistic so the given // problem can often be solved even if we warn that overflow is // possible. // // We don't use the interface from // operations_research/algorithms/hungarian.h because we want to be // able to express sparse problems efficiently. // // When asked to solve the given assignment problem we return a // boolean to indicate whether the given problem was feasible. // // References: // [ Goldberg and Kennedy's CSA paper ] A. V. Goldberg and R. Kennedy, // "An Efficient Cost Scaling Algorithm for the Assignment Problem." // Mathematical Programming, Vol. 71, pages 153-178, December 1995. // // [ Goldberg and Tarjan ] A. V. Goldberg and R. E. Tarjan, "Finding // Minimum-Cost Circulations by Successive Approximation." Mathematics // of Operations Research, Vol. 15, No. 3, pages 430-466, August 1990. // // [ Kennedy's thesis ] J. R. Kennedy, Jr., "Solving Unweighted and // Weighted Bipartite Matching Problems in Theory and Practice." // Stanford University Doctoral Dissertation, Department of Computer // Science, 1995. // // [ Burkard et al. ] R. Burkard, M. Dell'Amico, S. Martello, "Assignment // Problems", SIAM, 2009, ISBN: 978-0898716634, // http://www.amazon.com/dp/0898716632/ // // [ Ahuja et al. ] R. K. Ahuja, T. L. Magnanti, J. B. Orlin, "Network Flows: // Theory, Algorithms, and Applications," Prentice Hall, 1993, // ISBN: 978-0136175490, http://www.amazon.com/dp/013617549X. // // Keywords: linear sum assignment problem, Hungarian method, Goldberg, Kennedy. #ifndef OR_TOOLS_GRAPH_LINEAR_ASSIGNMENT_H_ #define OR_TOOLS_GRAPH_LINEAR_ASSIGNMENT_H_ #include #include #include #include #include #include #include #include #include "absl/strings/str_format.h" #include "ortools/base/commandlineflags.h" #include "ortools/base/integral_types.h" #include "ortools/base/logging.h" #include "ortools/base/macros.h" #include "ortools/graph/ebert_graph.h" #include "ortools/util/permutation.h" #include "ortools/util/zvector.h" #ifndef SWIG DECLARE_int64(assignment_alpha); DECLARE_int32(assignment_progress_logging_period); DECLARE_bool(assignment_stack_order); #endif namespace operations_research { // This class does not take ownership of its underlying graph. template class LinearSumAssignment { public: typedef typename GraphType::NodeIndex NodeIndex; typedef typename GraphType::ArcIndex ArcIndex; // Constructor for the case in which we will build the graph // incrementally as we discover arc costs, as might be done with any // of the dynamic graph representations such as StarGraph or ForwardStarGraph. LinearSumAssignment(const GraphType& graph, NodeIndex num_left_nodes); // Constructor for the case in which the underlying graph cannot be // built until after all the arc costs are known, as is the case // with ForwardStarStaticGraph. In this case, the graph is passed to // us later via the SetGraph() method, below. LinearSumAssignment(NodeIndex num_left_nodes, ArcIndex num_arcs); ~LinearSumAssignment() {} // Sets the graph used by the LinearSumAssignment instance, for use // when the graph layout can be determined only after arc costs are // set. This happens, for example, when we use a ForwardStarStaticGraph. void SetGraph(const GraphType* graph) { DCHECK(graph_ == nullptr); graph_ = graph; } // Sets the cost-scaling divisor, i.e., the amount by which we // divide the scaling parameter on each iteration. void SetCostScalingDivisor(CostValue factor) { alpha_ = factor; } // Returns a permutation cycle handler that can be passed to the // TransformToForwardStaticGraph method so that arc costs get // permuted along with arcs themselves. // // Passes ownership of the cycle handler to the caller. // operations_research::PermutationCycleHandler* ArcAnnotationCycleHandler(); // Optimizes the layout of the graph for the access pattern our // implementation will use. // // REQUIRES for LinearSumAssignment template instantiation if a call // to the OptimizeGraphLayout() method is compiled: GraphType is a // dynamic graph, i.e., one that implements the // GroupForwardArcsByFunctor() member template method. // // If analogous optimization is needed for LinearSumAssignment // instances based on static graphs, the graph layout should be // constructed such that each node's outgoing arcs are sorted by // head node index before the // LinearSumAssignment::SetGraph() method is called. void OptimizeGraphLayout(GraphType* graph); // Allows tests, iterators, etc., to inspect our underlying graph. inline const GraphType& Graph() const { return *graph_; } // These handy member functions make the code more compact, and we // expose them to clients so that client code that doesn't have // direct access to the graph can learn about the optimum assignment // once it is computed. inline NodeIndex Head(ArcIndex arc) const { return graph_->Head(arc); } // Returns the original arc cost for use by a client that's // iterating over the optimum assignment. CostValue ArcCost(ArcIndex arc) const { DCHECK_EQ(0, scaled_arc_cost_[arc] % cost_scaling_factor_); return scaled_arc_cost_[arc] / cost_scaling_factor_; } // Sets the cost of an arc already present in the given graph. void SetArcCost(ArcIndex arc, CostValue cost); // Completes initialization after the problem is fully specified. // Returns true if we successfully prove that arithmetic // calculations are guaranteed not to overflow. ComputeAssignment() // calls this method itself, so only clients that care about // obtaining a warning about the possibility of arithmetic precision // problems need to call this method explicitly. // // Separate from ComputeAssignment() for white-box testing and for // clients that need to react to the possibility that arithmetic // overflow is not ruled out. // // FinalizeSetup() is idempotent. bool FinalizeSetup(); // Computes the optimum assignment. Returns true on success. Return // value of false implies the given problem is infeasible. bool ComputeAssignment(); // Returns the cost of the minimum-cost perfect matching. // Precondition: success_ == true, signifying that we computed the // optimum assignment for a feasible problem. CostValue GetCost() const; // Returns the total number of nodes in the given problem. NodeIndex NumNodes() const { if (graph_ == nullptr) { // Return a guess that must be true if ultimately we are given a // feasible problem to solve. return 2 * NumLeftNodes(); } else { return graph_->num_nodes(); } } // Returns the number of nodes on the left side of the given // problem. NodeIndex NumLeftNodes() const { return num_left_nodes_; } // Returns the arc through which the given node is matched. inline ArcIndex GetAssignmentArc(NodeIndex left_node) const { DCHECK_LT(left_node, num_left_nodes_); return matched_arc_[left_node]; } // Returns the cost of the assignment arc incident to the given // node. inline CostValue GetAssignmentCost(NodeIndex node) const { return ArcCost(GetAssignmentArc(node)); } // Returns the node to which the given node is matched. inline NodeIndex GetMate(NodeIndex left_node) const { DCHECK_LT(left_node, num_left_nodes_); ArcIndex matching_arc = GetAssignmentArc(left_node); DCHECK_NE(GraphType::kNilArc, matching_arc); return Head(matching_arc); } std::string StatsString() const { return total_stats_.StatsString(); } class BipartiteLeftNodeIterator { public: BipartiteLeftNodeIterator(const GraphType& graph, NodeIndex num_left_nodes) : num_left_nodes_(num_left_nodes), node_iterator_(0) {} explicit BipartiteLeftNodeIterator(const LinearSumAssignment& assignment) : num_left_nodes_(assignment.NumLeftNodes()), node_iterator_(0) {} NodeIndex Index() const { return node_iterator_; } bool Ok() const { return node_iterator_ < num_left_nodes_; } void Next() { ++node_iterator_; } private: const NodeIndex num_left_nodes_; typename GraphType::NodeIndex node_iterator_; }; private: struct Stats { Stats() : pushes_(0), double_pushes_(0), relabelings_(0), refinements_(0) {} void Clear() { pushes_ = 0; double_pushes_ = 0; relabelings_ = 0; refinements_ = 0; } void Add(const Stats& that) { pushes_ += that.pushes_; double_pushes_ += that.double_pushes_; relabelings_ += that.relabelings_; refinements_ += that.refinements_; } std::string StatsString() const { return absl::StrFormat( "%d refinements; %d relabelings; " "%d double pushes; %d pushes", refinements_, relabelings_, double_pushes_, pushes_); } int64 pushes_; int64 double_pushes_; int64 relabelings_; int64 refinements_; }; #ifndef SWIG class ActiveNodeContainerInterface { public: virtual ~ActiveNodeContainerInterface() {} virtual bool Empty() const = 0; virtual void Add(NodeIndex node) = 0; virtual NodeIndex Get() = 0; }; class ActiveNodeStack : public ActiveNodeContainerInterface { public: ~ActiveNodeStack() override {} bool Empty() const override { return v_.empty(); } void Add(NodeIndex node) override { v_.push_back(node); } NodeIndex Get() override { DCHECK(!Empty()); NodeIndex result = v_.back(); v_.pop_back(); return result; } private: std::vector v_; }; class ActiveNodeQueue : public ActiveNodeContainerInterface { public: ~ActiveNodeQueue() override {} bool Empty() const override { return q_.empty(); } void Add(NodeIndex node) override { q_.push_front(node); } NodeIndex Get() override { DCHECK(!Empty()); NodeIndex result = q_.back(); q_.pop_back(); return result; } private: std::deque q_; }; #endif // Type definition for a pair // (arc index, reduced cost gap) // giving the arc along which we will push from a given left-side // node and the gap between that arc's partial reduced cost and the // reduced cost of the next-best (necessarily residual) arc out of // the node. This information helps us efficiently relabel // right-side nodes during DoublePush operations. typedef std::pair ImplicitPriceSummary; // Returns true if and only if the current pseudoflow is // epsilon-optimal. To be used in a DCHECK. bool EpsilonOptimal() const; // Checks that all nodes are matched. // To be used in a DCHECK. bool AllMatched() const; // Calculates the implicit price of the given node. // Only for debugging, for use in EpsilonOptimal(). inline CostValue ImplicitPrice(NodeIndex left_node) const; // For use by DoublePush() inline ImplicitPriceSummary BestArcAndGap(NodeIndex left_node) const; // Accumulates stats between iterations and reports them if the // verbosity level is high enough. void ReportAndAccumulateStats(); // Utility function to compute the next error parameter value. This // is used to ensure that the same sequence of error parameter // values is used for computation of price bounds as is used for // computing the optimum assignment. CostValue NewEpsilon(CostValue current_epsilon) const; // Advances internal state to prepare for the next scaling // iteration. Returns false if infeasibility is detected, true // otherwise. bool UpdateEpsilon(); // Indicates whether the given left_node has positive excess. Called // only for nodes on the left side. inline bool IsActive(NodeIndex left_node) const; // Indicates whether the given node has nonzero excess. The idea // here is the same as the IsActive method above, but that method // contains a safety DCHECK() that its argument is a left-side node, // while this method is usable for any node. // To be used in a DCHECK. inline bool IsActiveForDebugging(NodeIndex node) const; // Performs the push/relabel work for one scaling iteration. bool Refine(); // Puts all left-side nodes in the active set in preparation for the // first scaling iteration. void InitializeActiveNodeContainer(); // Saturates all negative-reduced-cost arcs at the beginning of each // scaling iteration. Note that according to the asymmetric // definition of admissibility, this action is different from // saturating all admissible arcs (which we never do). All negative // arcs are admissible, but not all admissible arcs are negative. It // is alwsys enough to saturate only the negative ones. void SaturateNegativeArcs(); // Performs an optimized sequence of pushing a unit of excess out of // the left-side node v and back to another left-side node if no // deficit is cancelled with the first push. bool DoublePush(NodeIndex source); // Returns the partial reduced cost of the given arc. inline CostValue PartialReducedCost(ArcIndex arc) const { return scaled_arc_cost_[arc] - price_[Head(arc)]; } // The graph underlying the problem definition we are given. Not // owned by *this. const GraphType* graph_; // The number of nodes on the left side of the graph we are given. NodeIndex num_left_nodes_; // A flag indicating, after FinalizeSetup() has run, whether the // arc-incidence precondition required by BestArcAndGap() is // satisfied by every left-side node. If not, the problem is // infeasible. bool incidence_precondition_satisfied_; // A flag indicating that an optimal perfect matching has been computed. bool success_; // The value by which we multiply all the arc costs we are given in // order to be able to use integer arithmetic in all our // computations. In order to establish optimality of the final // matching we compute, we need that // (cost_scaling_factor_ / kMinEpsilon) > graph_->num_nodes(). const CostValue cost_scaling_factor_; // Scaling divisor. CostValue alpha_; // Minimum value of epsilon. When a flow is epsilon-optimal for // epsilon == kMinEpsilon, the flow is optimal. static const CostValue kMinEpsilon; // Current value of epsilon, the cost scaling parameter. CostValue epsilon_; // The following two data members, price_lower_bound_ and // slack_relabeling_price_, have to do with bounds on the amount by // which node prices can change during execution of the algorithm. // We need some detailed discussion of this topic because we violate // several simplifying assumptions typically made in the theoretical // literature. In particular, we use integer arithmetic, we use a // reduction to the transportation problem rather than min-cost // circulation, we provide detection of infeasible problems rather // than assume feasibility, we detect when our computations might // exceed the range of representable cost values, and we use the // double-push heuristic which relabels nodes that do not have // excess. // // In the following discussion, we prove the following propositions: // Proposition 1. [Fidelity of arithmetic precision guarantee] If // FinalizeSetup() returns true, no arithmetic // overflow occurs during ComputeAssignment(). // Proposition 2. [Fidelity of feasibility detection] If no // arithmetic overflow occurs during // ComputeAssignment(), the return value of // ComputeAssignment() faithfully indicates whether // the given problem is feasible. // // We begin with some general discussion. // // The ideas used to prove our two propositions are essentially // those that appear in [Goldberg and Tarjan], but several details // are different: [Goldberg and Tarjan] assumes a feasible problem, // uses a symmetric notion of epsilon-optimality, considers only // nodes with excess eligible for relabeling, and does not treat the // question of arithmetic overflow. This implementation, on the // other hand, detects and reports infeasible problems, uses // asymmetric epsilon-optimality, relabels nodes with no excess in // the course of the double-push operation, and gives a reasonably // tight guarantee of arithmetic precision. No fundamentally new // ideas are involved, but the details are a bit tricky so they are // explained here. // // We have two intertwined needs that lead us to compute bounds on // the prices nodes can have during the assignment computation, on // the assumption that the given problem is feasible: // 1. Infeasibility detection: Infeasibility is detected by // observing that some node's price has been reduced too much by // relabeling operations (see [Goldberg and Tarjan] for the // argument -- duplicated in modified form below -- bounding the // running time of the push/relabel min-cost flow algorithm for // feasible problems); and // 2. Aggressively relabeling nodes and arcs whose matching is // forced: When a left-side node is incident to only one arc a, // any feasible solution must include a, and reducing the price // of Head(a) by any nonnegative amount preserves epsilon- // optimality. Because of this freedom, we'll call this sort of // relabeling (i.e., a relabeling of a right-side node that is // the only neighbor of the left-side node to which it has been // matched in the present double-push operation) a "slack" // relabeling. Relabelings that are not slack relabelings are // called "confined" relabelings. By relabeling Head(a) to have // p(Head(a))=-infinity, we could guarantee that a never becomes // unmatched during the current iteration, and this would prevent // our wasting time repeatedly unmatching and rematching a. But // there are some details we need to handle: // a. The CostValue type cannot represent -infinity; // b. Low node prices are precisely the signal we use to detect // infeasibility (see (1)), so we must be careful not to // falsely conclude that the problem is infeasible as a result // of the low price we gave Head(a); and // c. We need to indicate accurately to the client when our best // understanding indicates that we can't rule out arithmetic // overflow in our calculations. Most importantly, if we don't // warn the client, we must be certain to avoid overflow. This // means our slack relabelings must not be so aggressive as to // create the possibility of unforeseen overflow. Although we // will not achieve this in practice, slack relabelings would // ideally not introduce overflow unless overflow was // inevitable were even the smallest reasonable price change // (== epsilon) used for slack relabelings. // Using the analysis below, we choose a finite amount of price // change for slack relabelings aggressive enough that we don't // waste time doing repeated slack relabelings in a single // iteration, yet modest enough that we keep a good handle on // arithmetic precision and our ability to detect infeasible // problems. // // To provide faithful detection of infeasibility, a dependable // guarantee of arithmetic precision whenever possible, and good // performance by aggressively relabeling nodes whose matching is // forced, we exploit these facts: // 1. Beyond the first iteration, infeasibility detection isn't needed // because a problem is feasible in some iteration if and only if // it's feasible in all others. Therefore we are free to use an // infeasibility detection mechanism that might work in just one // iteration and switch it off in all other iterations. // 2. When we do a slack relabeling, we must choose the amount of // price reduction to use. We choose an amount large enough to // guarantee putting the node's matching to rest, yet (although // we don't bother to prove this explicitly) small enough that // the node's price obeys the overall lower bound that holds if // the slack relabeling amount is small. // // We will establish Propositions (1) and (2) above according to the // following steps: // First, we prove Lemma 1, which is a modified form of lemma 5.8 of // [Goldberg and Tarjan] giving a bound on the difference in price // between the end nodes of certain paths in the residual graph. // Second, we prove Lemma 2, which is technical lemma to establish // reachability of certain "anchor" nodes in the residual graph from // any node where a relabeling takes place. // Third, we apply the first two lemmas to prove Lemma 3 and Lemma // 4, which give two similar bounds that hold whenever the given // problem is feasible: (for feasibility detection) a bound on the // price of any node we relabel during any iteration (and the first // iteration in particular), and (for arithmetic precision) a bound // on the price of any node we relabel during the entire algorithm. // // Finally, we note that if the whole-algorithm price bound can be // represented precisely by the CostValue type, arithmetic overflow // cannot occur (establishing Proposition 1), and assuming no // overflow occurs during the first iteration, any violation of the // first-iteration price bound establishes infeasibility // (Proposition 2). // // The statement of Lemma 1 is perhaps easier to understand when the // reader knows how it will be used. To wit: In this lemma, f' and // e_0 are the flow and error parameter (epsilon) at the beginning // of the current iteration, while f and e_1 are the current // pseudoflow and error parameter when a relabeling of interest // occurs. Without loss of generality, c is the reduced cost // function at the beginning of the current iteration and p is the // change in prices that has taken place in the current iteration. // // Lemma 1 (a variant of lemma 5.8 from [Goldberg and Tarjan]): Let // f be a pseudoflow and let f' be a flow. Suppose P is a simple // path from right-side node v to right-side node w such that P is // residual with respect to f and reverse(P) is residual with // respect to f'. Further, suppose c is an arc cost function with // respect to which f' is e_0-optimal with the zero price function // and p is a price function with respect to which f is e_1-optimal // with respect to p. Then // p(v) - p(w) >= -(e_0 + e_1) * (n-2)/2. (***) // // Proof: We have c_p(P) = p(v) + c(P) - p(w) and hence // p(v) - p(w) = c_p(P) - c(P). // So we seek a bound on c_p(P) - c(P). // p(v) = c_p(P) - c(P). // Let arc a lie on P, which implies that a is residual with respect // to f and reverse(a) is residual with respect to f'. // Case 1: a is a forward arc. Then by e_1-optimality of f with // respect to p, c_p(a) >= 0 and reverse(a) is residual with // respect to f'. By e_0-optimality of f', c(a) <= e_0. So // c_p(a) - c(a) >= -e_0. // Case 2: a is a reverse arc. Then by e_1-optimality of f with // respect to p, c_p(a) >= -e_1 and reverse(a) is residual // with respect to f'. By e_0-optimality of f', c(a) <= 0. // So // c_p(a) - c(a) >= -e_1. // We assumed v and w are both right-side nodes, so there are at // most n - 2 arcs on the path P, of which at most (n-2)/2 are // forward arcs and at most (n-2)/2 are reverse arcs, so // p(v) - p(w) = c_p(P) - c(P) // >= -(e_0 + e_1) * (n-2)/2. (***) // // Some of the rest of our argument is given as a sketch, omitting // several details. Also elided here are some minor technical issues // related to the first iteration, inasmuch as our arguments assume // on the surface a "previous iteration" that doesn't exist in that // case. The issues are not substantial, just a bit messy. // // Lemma 2 is analogous to lemma 5.7 of [Goldberg and Tarjan], where // they have only relabelings that take place at nodes with excess // while we have only relabelings that take place as part of the // double-push operation at nodes without excess. // // Lemma 2: If the problem is feasible, for any node v with excess, // there exists a path P from v to a node w with deficit such that P // is residual with respect to the current pseudoflow, and // reverse(P) is residual with respect to the flow at the beginning // of the current iteration. (Note that such a path exactly // satisfies the conditions of Lemma 1.) // // Let the bound from Lemma 1 with p(w) = 0 be called B(e_0, e_1), // and let us say that when a slack relabeling of a node v occurs, // we will change the price of v by B(e_0, e_1) such that v tightly // satisfies the bound of Lemma 1. Explicitly, we define // B(e_0, e_1) = -(e_0 + e_1) * (n-2)/2. // // Lemma 1 and Lemma 2 combine to bound the price change during an // iteration for any node with excess. Viewed a different way, Lemma // 1 and Lemma 2 tell us that if epsilon-optimality can be preserved // by changing the price of a node by B(e_0, e_1), that node will // never have excess again during the current iteration unless the // problem is infeasible. This insight gives us an approach to // detect infeasibility (by observing prices on nodes with excess // that violate this bound) and to relabel nodes aggressively enough // to avoid unnecessary future work while we also avoid falsely // concluding the problem is infeasible. // // From Lemma 1 and Lemma 2, and taking into account our knowledge // of the slack relabeling amount, we have Lemma 3. // // Lemma 3: During any iteration, if the given problem is feasible // the price of any node is reduced by less than // -2 * B(e_0, e_1) = (e_0 + e_1) * (n-2). // // Proof: Straightforward, omitted for expedience. // // In the case where e_0 = e_1 * alpha, we can express the bound // just in terms of e_1, the current iteration's value of epsilon_: // B(e_1) = B(e_1 * alpha, e_1) = -(1 + alpha) * e_1 * (n-2)/2, // so we have that p(v) is reduced by less than 2 * B(e_1). // // Because we use truncating division to compute each iteration's error // parameter from that of the previous iteration, it isn't exactly // the case that e_0 = e_1 * alpha as we just assumed. To patch this // up, we can use the observation that // e_1 = floor(e_0 / alpha), // which implies // -e_0 > -(e_1 + 1) * alpha // to rewrite from (***): // p(v) > 2 * B(e_0, e_1) > 2 * B((e_1 + 1) * alpha, e_1) // = 2 * -((e_1 + 1) * alpha + e_1) * (n-2)/2 // = 2 * -(1 + alpha) * e_1 * (n-2)/2 - alpha * (n-2) // = 2 * B(e_1) - alpha * (n-2) // = -((1 + alpha) * e_1 + alpha) * (n-2). // // We sum up the bounds for all the iterations to get Lemma 4: // // Lemma 4: If the given problem is feasible, after k iterations the // price of any node is always greater than // -((1 + alpha) * C + (k * alpha)) * (n-2) // // Proof: Suppose the price decrease of every node in the iteration // with epsilon_ == x is bounded by B(x) which is proportional to x // (not surprisingly, this will be the same function B() as // above). Assume for simplicity that C, the largest cost magnitude, // is a power of alpha. Then the price of each node, tallied across // all iterations is bounded // p(v) > 2 * B(C/alpha) + 2 * B(C/alpha^2) + ... + 2 * B(kMinEpsilon) // == 2 * B(C/alpha) * alpha / (alpha - 1) // == 2 * B(C) / (alpha - 1). // As above, this needs some patching up to handle the fact that we // use truncating arithmetic. We saw that each iteration effectively // reduces the price bound by alpha * (n-2), hence if there are k // iterations, the bound is // p(v) > 2 * B(C) / (alpha - 1) - k * alpha * (n-2) // = -(1 + alpha) * C * (n-2) / (alpha - 1) - k * alpha * (n-2) // = (n-2) * (C * (1 + alpha) / (1 - alpha) - k * alpha). // // The bound of lemma 4 can be used to warn for possible overflow of // arithmetic precision. But because it involves the number of // iterations, k, we might as well count through the iterations // simply adding up the bounds given by Lemma 3 to get a tighter // result. This is what the implementation does. // A lower bound on the price of any node at any time throughout the // computation. A price below this level proves infeasibility; this // value is used for feasibility detection. We use this value also // to rule out the possibility of arithmetic overflow or warn the // client that we have not been able to rule out that possibility. // // We can use the value implied by Lemma 4 here, but note that that // value includes k, the number of iterations. It's plenty fast if // we count through the iterations to compute that value, but if // we're going to count through the iterations, we might as well use // the two-parameter bound from Lemma 3, summing up as we go. This // gives us a tighter bound and more comprehensible code. // // While computing this bound, if we find the value justified by the // theory lies outside the representable range of CostValue, we // conclude that the given arc costs have magnitudes so large that // we cannot guarantee our calculations don't overflow. If the value // justified by the theory lies inside the representable range of // CostValue, we commit that our calculation will not overflow. This // commitment means we need to be careful with the amount by which // we relabel right-side nodes that are incident to any node with // only one neighbor. CostValue price_lower_bound_; // A bound on the amount by which a node's price can be reduced // during the current iteration, used only for slack // relabelings. Where epsilon is the first iteration's error // parameter and C is the largest magnitude of an arc cost, we set // slack_relabeling_price_ = -B(C, epsilon) // = (C + epsilon) * (n-2)/2. // // We could use slack_relabeling_price_ for feasibility detection // but the feasibility threshold is double the slack relabeling // amount and we judge it not to be worth having to multiply by two // gratuitously to check feasibility in each double push // operation. Instead we settle for feasibility detection using // price_lower_bound_ instead, which is somewhat slower in the // infeasible case because more relabelings will be required for // some node price to attain the looser bound. CostValue slack_relabeling_price_; // Computes the value of the bound on price reduction for an // iteration, given the old and new values of epsilon_. Because the // expression computed here is used in at least one place where we // want an additional factor in the denominator, we take that factor // as an argument. If extra_divisor == 1, this function computes of // the function B() discussed above. // // Avoids overflow in computing the bound, and sets *in_range = // false if the value of the bound doesn't fit in CostValue. inline CostValue PriceChangeBound(CostValue old_epsilon, CostValue new_epsilon, bool* in_range) const { const CostValue n = graph_->num_nodes(); // We work in double-precision floating point to determine whether // we'll overflow the integral CostValue type's range of // representation. Switching between integer and double is a // rather expensive operation, but we do this only twice per // scaling iteration, so we can afford it rather than resort to // complex and subtle tricks within the bounds of integer // arithmetic. // // You will want to read the comments above about // price_lower_bound_ and slack_relabeling_price_, and have a // pencil handy. :-) const double result = static_cast(std::max(1, n / 2 - 1)) * (static_cast(old_epsilon) + static_cast(new_epsilon)); const double limit = static_cast(std::numeric_limits::max()); if (result > limit) { // Our integer computations could overflow. if (in_range != nullptr) *in_range = false; return std::numeric_limits::max(); } else { // Don't touch *in_range; other computations could already have // set it to false and we don't want to overwrite that result. return static_cast(result); } } // A scaled record of the largest arc-cost magnitude we've been // given during problem setup. This is used to set the initial value // of epsilon_, which in turn is used not only as the error // parameter but also to determine whether we risk arithmetic // overflow during the algorithm. // // Note: Our treatment of arithmetic overflow assumes the following // property of CostValue: // -std::numeric_limits::max() is a representable // CostValue. // That property is satisfied if CostValue uses a two's-complement // representation. CostValue largest_scaled_cost_magnitude_; // The total excess in the graph. Given our asymmetric definition of // epsilon-optimality and our use of the double-push operation, this // equals the number of unmatched left-side nodes. NodeIndex total_excess_; // Indexed by node index, the price_ values are maintained only for // right-side nodes. // // Note: We use a ZVector to only allocate a vector of size num_left_nodes_ // instead of 2*num_left_nodes_ since the right-side node indices start at // num_left_nodes_. ZVector price_; // Indexed by left-side node index, the matched_arc_ array gives the // arc index of the arc matching any given left-side node, or // GraphType::kNilArc if the node is unmatched. std::vector matched_arc_; // Indexed by right-side node index, the matched_node_ array gives // the node index of the left-side node matching any given // right-side node, or GraphType::kNilNode if the right-side node is // unmatched. // // Note: We use a ZVector for the same reason as for price_. ZVector matched_node_; // The array of arc costs as given in the problem definition, except // that they are scaled up by the number of nodes in the graph so we // can use integer arithmetic throughout. std::vector scaled_arc_cost_; // The container of active nodes (i.e., unmatched nodes). This can // be switched easily between ActiveNodeStack and ActiveNodeQueue // for experimentation. std::unique_ptr active_nodes_; // Statistics giving the overall numbers of various operations the // algorithm performs. Stats total_stats_; // Statistics giving the numbers of various operations the algorithm // has performed in the current iteration. Stats iteration_stats_; DISALLOW_COPY_AND_ASSIGN(LinearSumAssignment); }; // Implementation of out-of-line LinearSumAssignment template member // functions. template const CostValue LinearSumAssignment::kMinEpsilon = 1; template LinearSumAssignment::LinearSumAssignment( const GraphType& graph, const NodeIndex num_left_nodes) : graph_(&graph), num_left_nodes_(num_left_nodes), success_(false), cost_scaling_factor_(1 + num_left_nodes), alpha_(FLAGS_assignment_alpha), epsilon_(0), price_lower_bound_(0), slack_relabeling_price_(0), largest_scaled_cost_magnitude_(0), total_excess_(0), price_(num_left_nodes, 2 * num_left_nodes - 1), matched_arc_(num_left_nodes, 0), matched_node_(num_left_nodes, 2 * num_left_nodes - 1), scaled_arc_cost_(graph.max_end_arc_index(), 0), active_nodes_(FLAGS_assignment_stack_order ? static_cast( new ActiveNodeStack()) : static_cast( new ActiveNodeQueue())) {} template LinearSumAssignment::LinearSumAssignment( const NodeIndex num_left_nodes, const ArcIndex num_arcs) : graph_(nullptr), num_left_nodes_(num_left_nodes), success_(false), cost_scaling_factor_(1 + num_left_nodes), alpha_(FLAGS_assignment_alpha), epsilon_(0), price_lower_bound_(0), slack_relabeling_price_(0), largest_scaled_cost_magnitude_(0), total_excess_(0), price_(num_left_nodes, 2 * num_left_nodes - 1), matched_arc_(num_left_nodes, 0), matched_node_(num_left_nodes, 2 * num_left_nodes - 1), scaled_arc_cost_(num_arcs, 0), active_nodes_(FLAGS_assignment_stack_order ? static_cast( new ActiveNodeStack()) : static_cast( new ActiveNodeQueue())) {} template void LinearSumAssignment::SetArcCost(ArcIndex arc, CostValue cost) { if (graph_ != nullptr) { DCHECK_GE(arc, 0); DCHECK_LT(arc, graph_->num_arcs()); NodeIndex head = Head(arc); DCHECK_LE(num_left_nodes_, head); } cost *= cost_scaling_factor_; const CostValue cost_magnitude = std::abs(cost); largest_scaled_cost_magnitude_ = std::max(largest_scaled_cost_magnitude_, cost_magnitude); scaled_arc_cost_[arc] = cost; } template class CostValueCycleHandler : public PermutationCycleHandler { public: explicit CostValueCycleHandler(std::vector* cost) : temp_(0), cost_(cost) {} void SetTempFromIndex(ArcIndexType source) override { temp_ = (*cost_)[source]; } void SetIndexFromIndex(ArcIndexType source, ArcIndexType destination) const override { (*cost_)[destination] = (*cost_)[source]; } void SetIndexFromTemp(ArcIndexType destination) const override { (*cost_)[destination] = temp_; } ~CostValueCycleHandler() override {} private: CostValue temp_; std::vector* const cost_; DISALLOW_COPY_AND_ASSIGN(CostValueCycleHandler); }; // Logically this class should be defined inside OptimizeGraphLayout, // but compilation fails if we do that because C++98 doesn't allow // instantiation of member templates with function-scoped types as // template parameters, which in turn is because those function-scoped // types lack linkage. template class ArcIndexOrderingByTailNode { public: explicit ArcIndexOrderingByTailNode(const GraphType& graph) : graph_(graph) {} // Says ArcIndex a is less than ArcIndex b if arc a's tail is less // than arc b's tail. If their tails are equal, orders according to // heads. bool operator()(typename GraphType::ArcIndex a, typename GraphType::ArcIndex b) const { return ((graph_.Tail(a) < graph_.Tail(b)) || ((graph_.Tail(a) == graph_.Tail(b)) && (graph_.Head(a) < graph_.Head(b)))); } private: const GraphType& graph_; // Copy and assign are allowed; they have to be for STL to work // with this functor, although it seems like a bug for STL to be // written that way. }; // Passes ownership of the cycle handler to the caller. template PermutationCycleHandler* LinearSumAssignment::ArcAnnotationCycleHandler() { return new CostValueCycleHandler( &scaled_arc_cost_); } template void LinearSumAssignment::OptimizeGraphLayout(GraphType* graph) { // The graph argument is only to give us a non-const-qualified // handle on the graph we already have. Any different graph is // nonsense. DCHECK_EQ(graph_, graph); const ArcIndexOrderingByTailNode compare(*graph_); CostValueCycleHandler cycle_handler( &scaled_arc_cost_); TailArrayManager tail_array_manager(graph); tail_array_manager.BuildTailArrayFromAdjacencyListsIfForwardGraph(); graph->GroupForwardArcsByFunctor(compare, &cycle_handler); tail_array_manager.ReleaseTailArrayIfForwardGraph(); } template CostValue LinearSumAssignment::NewEpsilon( const CostValue current_epsilon) const { return std::max(current_epsilon / alpha_, kMinEpsilon); } template bool LinearSumAssignment::UpdateEpsilon() { CostValue new_epsilon = NewEpsilon(epsilon_); slack_relabeling_price_ = PriceChangeBound(epsilon_, new_epsilon, nullptr); epsilon_ = new_epsilon; VLOG(3) << "Updated: epsilon_ == " << epsilon_; VLOG(4) << "slack_relabeling_price_ == " << slack_relabeling_price_; DCHECK_GT(slack_relabeling_price_, 0); // For today we always return true; in the future updating epsilon // in sophisticated ways could conceivably detect infeasibility // before the first iteration of Refine(). return true; } // For production code that checks whether a left-side node is active. template inline bool LinearSumAssignment::IsActive( NodeIndex left_node) const { DCHECK_LT(left_node, num_left_nodes_); return matched_arc_[left_node] == GraphType::kNilArc; } // Only for debugging. Separate from the production IsActive() method // so that method can assert that its argument is a left-side node, // while for debugging we need to be able to test any node. template inline bool LinearSumAssignment::IsActiveForDebugging( NodeIndex node) const { if (node < num_left_nodes_) { return IsActive(node); } else { return matched_node_[node] == GraphType::kNilNode; } } template void LinearSumAssignment::InitializeActiveNodeContainer() { DCHECK(active_nodes_->Empty()); for (BipartiteLeftNodeIterator node_it(*graph_, num_left_nodes_); node_it.Ok(); node_it.Next()) { const NodeIndex node = node_it.Index(); if (IsActive(node)) { active_nodes_->Add(node); } } } // There exists a price function such that the admissible arcs at the // beginning of an iteration are exactly the reverse arcs of all // matching arcs. Saturating all admissible arcs with respect to that // price function therefore means simply unmatching every matched // node. // // In the future we will price out arcs, which will reduce the set of // nodes we unmatch here. If a matching arc is priced out, we will not // unmatch its endpoints since that element of the matching is // guaranteed not to change. template void LinearSumAssignment::SaturateNegativeArcs() { total_excess_ = 0; for (BipartiteLeftNodeIterator node_it(*graph_, num_left_nodes_); node_it.Ok(); node_it.Next()) { const NodeIndex node = node_it.Index(); if (IsActive(node)) { // This can happen in the first iteration when nothing is // matched yet. total_excess_ += 1; } else { // We're about to create a unit of excess by unmatching these nodes. total_excess_ += 1; const NodeIndex mate = GetMate(node); matched_arc_[node] = GraphType::kNilArc; matched_node_[mate] = GraphType::kNilNode; } } } // Returns true for success, false for infeasible. template bool LinearSumAssignment::DoublePush(NodeIndex source) { DCHECK_GT(num_left_nodes_, source); DCHECK(IsActive(source)) << "Node " << source << "must be active (unmatched)!"; ImplicitPriceSummary summary = BestArcAndGap(source); const ArcIndex best_arc = summary.first; const CostValue gap = summary.second; // Now we have the best arc incident to source, i.e., the one with // minimum reduced cost. Match that arc, unmatching its head if // necessary. if (best_arc == GraphType::kNilArc) { return false; } const NodeIndex new_mate = Head(best_arc); const NodeIndex to_unmatch = matched_node_[new_mate]; if (to_unmatch != GraphType::kNilNode) { // Unmatch new_mate from its current mate, pushing the unit of // flow back to a node on the left side as a unit of excess. matched_arc_[to_unmatch] = GraphType::kNilArc; active_nodes_->Add(to_unmatch); // This counts as a double push. iteration_stats_.double_pushes_ += 1; } else { // We are about to increase the cardinality of the matching. total_excess_ -= 1; // This counts as a single push. iteration_stats_.pushes_ += 1; } matched_arc_[source] = best_arc; matched_node_[new_mate] = source; // Finally, relabel new_mate. iteration_stats_.relabelings_ += 1; const CostValue new_price = price_[new_mate] - gap - epsilon_; price_[new_mate] = new_price; return new_price >= price_lower_bound_; } template bool LinearSumAssignment::Refine() { SaturateNegativeArcs(); InitializeActiveNodeContainer(); while (total_excess_ > 0) { // Get an active node (i.e., one with excess == 1) and discharge // it using DoublePush. const NodeIndex node = active_nodes_->Get(); if (!DoublePush(node)) { // Infeasibility detected. // // If infeasibility is detected after the first iteration, we // have a bug. We don't crash production code in this case but // we know we're returning a wrong answer so we we leave a // message in the logs to increase our hope of chasing down the // problem. LOG_IF(DFATAL, total_stats_.refinements_ > 0) << "Infeasibility detection triggered after first iteration found " << "a feasible assignment!"; return false; } } DCHECK(active_nodes_->Empty()); iteration_stats_.refinements_ += 1; return true; } // Computes best_arc, the minimum reduced-cost arc incident to // left_node and admissibility_gap, the amount by which the reduced // cost of best_arc must be increased to make it equal in reduced cost // to another residual arc incident to left_node. // // Precondition: left_node is unmatched and has at least one incident // arc. This allows us to simplify the code. The debug-only // counterpart to this routine is LinearSumAssignment::ImplicitPrice() // and it assumes there is an incident arc but does not assume the // node is unmatched. The condition that each left node has at least // one incident arc is explicitly computed during FinalizeSetup(). // // This function is large enough that our suggestion that the compiler // inline it might be pointless. template inline typename LinearSumAssignment::ImplicitPriceSummary LinearSumAssignment::BestArcAndGap(NodeIndex left_node) const { DCHECK(IsActive(left_node)) << "Node " << left_node << " must be active (unmatched)!"; DCHECK_GT(epsilon_, 0); typename GraphType::OutgoingArcIterator arc_it(*graph_, left_node); ArcIndex best_arc = arc_it.Index(); CostValue min_partial_reduced_cost = PartialReducedCost(best_arc); // We choose second_min_partial_reduced_cost so that in the case of // the largest possible gap (which results from a left-side node // with only a single incident residual arc), the corresponding // right-side node will be relabeled by an amount that exactly // matches slack_relabeling_price_. const CostValue max_gap = slack_relabeling_price_ - epsilon_; CostValue second_min_partial_reduced_cost = min_partial_reduced_cost + max_gap; for (arc_it.Next(); arc_it.Ok(); arc_it.Next()) { const ArcIndex arc = arc_it.Index(); const CostValue partial_reduced_cost = PartialReducedCost(arc); if (partial_reduced_cost < second_min_partial_reduced_cost) { if (partial_reduced_cost < min_partial_reduced_cost) { best_arc = arc; second_min_partial_reduced_cost = min_partial_reduced_cost; min_partial_reduced_cost = partial_reduced_cost; } else { second_min_partial_reduced_cost = partial_reduced_cost; } } } const CostValue gap = std::min( second_min_partial_reduced_cost - min_partial_reduced_cost, max_gap); DCHECK_GE(gap, 0); return std::make_pair(best_arc, gap); } // Only for debugging. // // Requires the precondition, explicitly computed in FinalizeSetup(), // that every left-side node has at least one incident arc. template inline CostValue LinearSumAssignment::ImplicitPrice( NodeIndex left_node) const { DCHECK_GT(num_left_nodes_, left_node); DCHECK_GT(epsilon_, 0); typename GraphType::OutgoingArcIterator arc_it(*graph_, left_node); // We must not execute this method if left_node has no incident arc. DCHECK(arc_it.Ok()); ArcIndex best_arc = arc_it.Index(); if (best_arc == matched_arc_[left_node]) { arc_it.Next(); if (arc_it.Ok()) { best_arc = arc_it.Index(); } } CostValue min_partial_reduced_cost = PartialReducedCost(best_arc); if (!arc_it.Ok()) { // Only one arc is incident to left_node, and the node is // currently matched along that arc, which must be the case in any // feasible solution. Therefore we implicitly price this node so // low that we will never consider unmatching it. return -(min_partial_reduced_cost + slack_relabeling_price_); } for (arc_it.Next(); arc_it.Ok(); arc_it.Next()) { const ArcIndex arc = arc_it.Index(); if (arc != matched_arc_[left_node]) { const CostValue partial_reduced_cost = PartialReducedCost(arc); if (partial_reduced_cost < min_partial_reduced_cost) { min_partial_reduced_cost = partial_reduced_cost; } } } return -min_partial_reduced_cost; } // Only for debugging. template bool LinearSumAssignment::AllMatched() const { for (NodeIndex node = 0; node < graph_->num_nodes(); ++node) { if (IsActiveForDebugging(node)) { return false; } } return true; } // Only for debugging. template bool LinearSumAssignment::EpsilonOptimal() const { for (BipartiteLeftNodeIterator node_it(*graph_, num_left_nodes_); node_it.Ok(); node_it.Next()) { const NodeIndex left_node = node_it.Index(); // Get the implicit price of left_node and make sure the reduced // costs of left_node's incident arcs are in bounds. CostValue left_node_price = ImplicitPrice(left_node); for (typename GraphType::OutgoingArcIterator arc_it(*graph_, left_node); arc_it.Ok(); arc_it.Next()) { const ArcIndex arc = arc_it.Index(); const CostValue reduced_cost = left_node_price + PartialReducedCost(arc); // Note the asymmetric definition of epsilon-optimality that we // use because it means we can saturate all admissible arcs in // the beginning of Refine() just by unmatching all matched // nodes. if (matched_arc_[left_node] == arc) { // The reverse arc is residual. Epsilon-optimality requires // that the reduced cost of the forward arc be at most // epsilon_. if (reduced_cost > epsilon_) { return false; } } else { // The forward arc is residual. Epsilon-optimality requires // that the reduced cost of the forward arc be at least zero. if (reduced_cost < 0) { return false; } } } } return true; } template bool LinearSumAssignment::FinalizeSetup() { incidence_precondition_satisfied_ = true; // epsilon_ must be greater than kMinEpsilon so that in the case // where the largest arc cost is zero, we still do a Refine() // iteration. epsilon_ = std::max(largest_scaled_cost_magnitude_, kMinEpsilon + 1); VLOG(2) << "Largest given cost magnitude: " << largest_scaled_cost_magnitude_ / cost_scaling_factor_; // Initialize left-side node-indexed arrays and check incidence // precondition. for (NodeIndex node = 0; node < num_left_nodes_; ++node) { matched_arc_[node] = GraphType::kNilArc; typename GraphType::OutgoingArcIterator arc_it(*graph_, node); if (!arc_it.Ok()) { incidence_precondition_satisfied_ = false; } } // Initialize right-side node-indexed arrays. Example: prices are // stored only for right-side nodes. for (NodeIndex node = num_left_nodes_; node < graph_->num_nodes(); ++node) { price_[node] = 0; matched_node_[node] = GraphType::kNilNode; } bool in_range = true; double double_price_lower_bound = 0.0; CostValue new_error_parameter; CostValue old_error_parameter = epsilon_; do { new_error_parameter = NewEpsilon(old_error_parameter); double_price_lower_bound -= 2.0 * static_cast(PriceChangeBound( old_error_parameter, new_error_parameter, &in_range)); old_error_parameter = new_error_parameter; } while (new_error_parameter != kMinEpsilon); const double limit = -static_cast(std::numeric_limits::max()); if (double_price_lower_bound < limit) { in_range = false; price_lower_bound_ = -std::numeric_limits::max(); } else { price_lower_bound_ = static_cast(double_price_lower_bound); } VLOG(4) << "price_lower_bound_ == " << price_lower_bound_; DCHECK_LE(price_lower_bound_, 0); if (!in_range) { LOG(WARNING) << "Price change bound exceeds range of representable " << "costs; arithmetic overflow is not ruled out and " << "infeasibility might go undetected."; } return in_range; } template void LinearSumAssignment::ReportAndAccumulateStats() { total_stats_.Add(iteration_stats_); VLOG(3) << "Iteration stats: " << iteration_stats_.StatsString(); iteration_stats_.Clear(); } template bool LinearSumAssignment::ComputeAssignment() { CHECK(graph_ != nullptr); bool ok = graph_->num_nodes() == 2 * num_left_nodes_; if (!ok) return false; // Note: FinalizeSetup() might have been called already by white-box // test code or by a client that wants to react to the possibility // of overflow before solving the given problem, but FinalizeSetup() // is idempotent and reasonably fast, so we call it unconditionally // here. FinalizeSetup(); ok = ok && incidence_precondition_satisfied_; DCHECK(!ok || EpsilonOptimal()); while (ok && epsilon_ > kMinEpsilon) { ok = ok && UpdateEpsilon(); ok = ok && Refine(); ReportAndAccumulateStats(); DCHECK(!ok || EpsilonOptimal()); DCHECK(!ok || AllMatched()); } success_ = ok; VLOG(1) << "Overall stats: " << total_stats_.StatsString(); return ok; } template CostValue LinearSumAssignment::GetCost() const { // It is illegal to call this method unless we successfully computed // an optimum assignment. DCHECK(success_); CostValue cost = 0; for (BipartiteLeftNodeIterator node_it(*this); node_it.Ok(); node_it.Next()) { cost += GetAssignmentCost(node_it.Index()); } return cost; } } // namespace operations_research #endif // OR_TOOLS_GRAPH_LINEAR_ASSIGNMENT_H_