Files
ortools-clone/graph/linear_assignment.cc
2011-06-24 12:09:14 +00:00

482 lines
18 KiB
C++

// Copyright 2010-2011 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 "graph/linear_assignment.h"
#include <algorithm>
#include <cstdlib>
#include "base/commandlineflags.h"
#include "base/logging.h"
#include "base/scoped_ptr.h"
#include "util/permutation.h"
DEFINE_int64(assignment_alpha, 5,
"Divisor for epsilon at each Refine "
"step of LinearSumAssignment.");
DEFINE_int32(assignment_progress_logging_period, 5000,
"Number of relabelings to do between logging progress messages "
"when verbose level is 4 or more.");
DEFINE_bool(assignment_stack_order, true,
"Process active nodes in stack (as opposed to queue) order.");
namespace operations_research {
const CostValue LinearSumAssignment::kMinEpsilon = 1;
LinearSumAssignment::LinearSumAssignment(const StarGraph& graph,
NodeIndex num_left_nodes)
: graph_(graph),
num_left_nodes_(num_left_nodes),
success_(false),
cost_scaling_factor_(1 + (graph.max_num_nodes() / 2)),
alpha_(FLAGS_assignment_alpha),
epsilon_(0),
price_lower_bound_(0),
price_reduction_bound_(0),
largest_scaled_cost_magnitude_(0),
total_excess_(0),
price_(num_left_nodes + StarGraph::kFirstNode,
graph.max_end_node_index() - 1),
matched_(StarGraph::kFirstNode,
graph.max_end_node_index() - 1),
scaled_arc_cost_(StarGraph::kFirstArc,
graph.max_end_arc_index() - 1),
active_nodes_(
FLAGS_assignment_stack_order ?
static_cast<ActiveNodeContainerInterface*>(new ActiveNodeStack()) :
static_cast<ActiveNodeContainerInterface*>(new ActiveNodeQueue())) { }
void LinearSumAssignment::SetArcCost(ArcIndex arc,
CostValue cost) {
DCHECK(graph_.CheckArcValidity(arc));
NodeIndex tail = Tail(arc);
NodeIndex head = Head(arc);
DCHECK_GT(num_left_nodes_, tail);
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_.Set(arc, cost);
}
class CostValueCycleHandler
: public PermutationCycleHandler<ArcIndex> {
public:
explicit CostValueCycleHandler(Int64PackedArray* cost)
: temp_(0),
cost_(cost) { }
virtual void SetTempFromIndex(ArcIndex source) {
temp_ = cost_->Value(source);
}
virtual void SetIndexFromIndex(ArcIndex source,
ArcIndex destination) const {
cost_->Set(destination, cost_->Value(source));
}
virtual void SetIndexFromTemp(ArcIndex destination) const {
cost_->Set(destination, temp_);
}
virtual ~CostValueCycleHandler() { }
private:
CostValue temp_;
Int64PackedArray* 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.
class ArcIndexOrderingByTailNode {
public:
explicit ArcIndexOrderingByTailNode(const StarGraph& 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()(ArcIndex a, ArcIndex b) const {
return ((graph_.Tail(a) < graph_.Tail(b)) ||
((graph_.Tail(a) == graph_.Tail(b)) &&
(graph_.Head(a) < graph_.Head(b))));
}
private:
const StarGraph& 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.
};
void LinearSumAssignment::OptimizeGraphLayout(StarGraph* 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_);
graph->GroupForwardArcsByFunctor(compare, &cycle_handler);
}
bool LinearSumAssignment::UpdateEpsilon() {
// There are some somewhat subtle issues around using integer
// arithmetic to compute successive values of epsilon_, the error
// parameter.
//
// First, the value of price_reduction_bound_ is chosen under the
// assumption that it is truly an upper bound on the amount by which
// a node's price can change during the current iteration. The value
// of this bound in turn depends on the assumption that the flow
// computed by the previous iteration was
// (epsilon_ * alpha_)-optimal. If epsilon_ decreases by more than a
// factor of alpha_ due to truncating arithmetic, that bound might
// not hold, and the consequence is that BestArcAndGap could return
// an overly cautious admissibility gap in the case where a
// left-side node has only one incident arc. This is not a big deal
// at all. At worst it could lead to a few extra relabelings.
//
// Second, it is not a problem currently, but in the future if we
// use an arc-fixing heuristic, we cannot permit truncating integer
// division to decrease epsilon_ by a factor greater than alpha_
// because our bounds on price changes (and hence on the reduced
// cost of an arc that might ever become admissible in the future)
// depend on the ratio between the values of the error parameter at
// successive iterations. One consequence will be that we might
// occasionally do an extra iteration today in the interest of being
// able to "price arcs out" (which we don't do today). Today we
// simply use truncating integer division, but note that this will
// have to change if we ever price arcs out.
//
// Since neither of those issues is a problem today, we simply use
// truncating integer arithmetic. But future changes might
// necessitate rounding epsilon upward in the division.
epsilon_ = std::max(epsilon_ / alpha_, kMinEpsilon);
VLOG(3) << "Updated: epsilon_ == " << epsilon_;
price_reduction_bound_ = PriceChangeBound(1, NULL);
DCHECK_GT(price_reduction_bound_, 0);
// For today we always return true; in the future updating epsilon
// in sophisticated ways could conceivably detect infeasibility.
return true;
}
inline bool LinearSumAssignment::IsActive(NodeIndex node) const {
// In a non-debug compilation we could assert that this is a
// left-side node, but in debug compilations we use this routine to
// check invariants for right-side nodes. So no assertion.
return matched_[node] == StarGraph::kNilArc;
}
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.
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_.Set(mate, StarGraph::kNilArc);
matched_.Set(node, StarGraph::kNilArc);
}
}
}
// Returns true for success, false for infeasible.
bool LinearSumAssignment::DoublePush(NodeIndex source) {
DCHECK(IsActive(source));
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 == StarGraph::kNilArc) {
return false;
}
const NodeIndex new_mate = Head(best_arc);
const ArcIndex to_unmatch = matched_[new_mate];
if (to_unmatch != StarGraph::kNilArc) {
// 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.
const NodeIndex destination = Tail(to_unmatch);
matched_.Set(destination, StarGraph::kNilArc);
active_nodes_->Add(destination);
// 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_.Set(source, best_arc);
matched_.Set(new_mate, best_arc);
// Finally, relabel new_mate.
iteration_stats_.relabelings_ += 1;
price_.Set(new_mate, price_[new_mate] - gap - epsilon_);
return price_[new_mate] >= price_lower_bound_;
}
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.
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. This allows us to simplify
// the code. The debug-only counterpart to this routine is
// LinearSumAssignment::ImplicitPrice() and it does not assume this
// precondition.
//
// This function is large enough that our suggestion that the compiler
// inline it might be pointless.
inline LinearSumAssignment::ImplicitPriceSummary
LinearSumAssignment::BestArcAndGap(NodeIndex left_node) const {
DCHECK(IsActive(left_node));
DCHECK_GT(epsilon_, 0);
// During any scaling iteration, the price of an active node
// decreases by at most price_reduction_bound_ and all left-side
// nodes are made active at the beginning of Refine(), so the bound
// applies to all left-side nodes.
StarGraph::OutgoingArcIterator arc_it(graph_, left_node);
ArcIndex best_arc = arc_it.Index();
CostValue min_partial_reduced_cost = PartialReducedCost(best_arc);
CostValue second_min_partial_reduced_cost =
min_partial_reduced_cost + price_reduction_bound_;
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 < min_partial_reduced_cost) {
best_arc = arc;
second_min_partial_reduced_cost = min_partial_reduced_cost;
min_partial_reduced_cost = partial_reduced_cost;
} else if (partial_reduced_cost < second_min_partial_reduced_cost) {
second_min_partial_reduced_cost = partial_reduced_cost;
}
}
const CostValue gap =
second_min_partial_reduced_cost - min_partial_reduced_cost;
DCHECK_GE(gap, 0);
return std::make_pair(best_arc, gap);
}
// Only for debugging.
inline CostValue
LinearSumAssignment::ImplicitPrice(NodeIndex left_node) const {
DCHECK_GT(epsilon_, 0);
StarGraph::OutgoingArcIterator arc_it(graph_, left_node);
// If the input problem is feasible, it is always the case that
// arc_it.Ok(), i.e., that there is at least one arc incident to
// left_node.
DCHECK(arc_it.Ok());
ArcIndex best_arc = arc_it.Index();
if (best_arc == matched_[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 + price_reduction_bound_);
}
for (arc_it.Next(); arc_it.Ok(); arc_it.Next()) {
const ArcIndex arc = arc_it.Index();
if (arc != matched_[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.
bool LinearSumAssignment::AllMatched() const {
for (StarGraph::NodeIterator node_it(graph_);
node_it.Ok();
node_it.Next()) {
if (IsActive(node_it.Index())) {
return false;
}
}
return true;
}
// Only for debugging.
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 (StarGraph::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_[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;
}
bool LinearSumAssignment::FinalizeSetup() {
epsilon_ = largest_scaled_cost_magnitude_;
VLOG(2) << "Largest given cost magnitude: " <<
largest_scaled_cost_magnitude_ / cost_scaling_factor_;
// Initialize left-side node-indexed arrays.
StarGraph::NodeIterator node_it(graph_);
for (; node_it.Ok(); node_it.Next()) {
const NodeIndex node = node_it.Index();
if (node >= num_left_nodes_) {
break;
}
matched_.Set(node, StarGraph::kNilArc);
}
// Initialize right-side node-indexed arrays. Example: prices are
// stored only for right-side nodes.
for (; node_it.Ok(); node_it.Next()) {
const NodeIndex node = node_it.Index();
price_.Set(node, 0);
matched_.Set(node, StarGraph::kNilArc);
}
bool in_range;
price_lower_bound_ = -PriceChangeBound(alpha_ - 1, &in_range);
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.";
}
return in_range;
}
void LinearSumAssignment::ReportAndAccumulateStats() {
total_stats_.Add(iteration_stats_);
VLOG(3) << "Iteration stats: " << iteration_stats_.StatsString();
iteration_stats_.Clear();
}
bool LinearSumAssignment::ComputeAssignment() {
// 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();
bool ok = graph_.num_nodes() == 2 * num_left_nodes_;
DCHECK(!ok || EpsilonOptimal());
while (ok && epsilon_ > kMinEpsilon) {
ok &= UpdateEpsilon();
ok &= Refine();
ReportAndAccumulateStats();
DCHECK(!ok || EpsilonOptimal());
DCHECK(!ok || AllMatched());
}
success_ = ok;
return ok;
}
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