Files
ortools-clone/ortools/set_cover/set_cover_cft.h
Corentin Le Molgat b4b226801b update include guards
2025-11-05 11:54:02 +01:00

458 lines
18 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2025 Francesco Cavaliere
// 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.
#ifndef ORTOOLS_SET_COVER_SET_COVER_CFT_H
#define ORTOOLS_SET_COVER_SET_COVER_CFT_H
#include <absl/algorithm/container.h>
#include <absl/base/internal/pretty_function.h>
#include <absl/status/status.h>
#include <limits>
#include "ortools/set_cover/base_types.h"
#include "ortools/set_cover/set_cover_submodel.h"
#include "ortools/set_cover/set_cover_views.h"
namespace operations_research::scp {
// Implementation of:
// Caprara, Alberto, Matteo Fischetti, and Paolo Toth. 1999. “A Heuristic
// Method for the Set Covering Problem.” Operations Research 47 (5): 73043.
// https://www.jstor.org/stable/223097
//
// Hereafter referred to as CFT.
//
// SUMMARY
// The CFT algorithm is a heuristic approach to the set-covering problem. At its
// core, it combines a primal greedy heuristic with dual information obtained
// from the optimization of the Lagrangian relaxation of the problem.
// (Note: columns/subsets and rows/elements will be used interchangeably.)
//
// STRUCTURE
// The core of the algorithm is the 3Phase:
// 1. Subgradient optimization of the Lagrangian relaxation.
// 2. A primal greedy heuristic guided by the dual information.
// 3. Fixing some of the "best" columns (in terms of reduced costs) into the
// solution (diving).
// + Repeat until an exit criterion is met.
//
// The paper also considers an optional outer loop, which invokes the 3Phase
// process and then fixes some columns from the current best solution. This
// introduces two levels of diving: the outer loop fixes "primal-good" columns
// (based on the best solution), while the inner loop fixes "dual-good" columns
// (based on reduced costs).
// NOTE: The outer loop is not implemented in this version (yet - April 2025).
//
// Key characteristics of the algorithm:
//
// - The CFT algorithm is tailored for instances where the number of columns is
// significantly larger than the number of rows.
//
// - To improve efficiency, a core model approach is used. This involves
// selecting a small subset of columns based on their reduced costs, thereby
// substantially reducing the problem size handled in the internal steps.
//
// - Due to the use of the core model and column fixing, the algorithm rarely
// considers the entire problem. Instead, it operates on a small "window" of
// the problem. Efficiently managing this small window is a central aspect of
// any CFT implementation.
//
// - The core model scheme also enables an alternative implementation where the
// algorithm starts with a small model and progressively adds columns through
// a column-generation procedure. While this column generation procedure is
// problem-dependent and cannot be implemented here, the architecture of this
// implementation is designed to be extensible, allowing for such a procedure
// to be added in the future.
//
////////////////////////////////////////////////////////////////////////
////////////////////////// COMMON DEFINITIONS //////////////////////////
////////////////////////////////////////////////////////////////////////
// Small class to store the solution of a sub-model. It contains the cost and
// the subset list.
class Solution {
public:
Solution() = default;
Solution(const SubModel& model, const std::vector<SubsetIndex>& core_subsets);
double cost() const { return cost_; }
const std::vector<FullSubsetIndex>& subsets() const { return subsets_; }
void AddSubset(FullSubsetIndex subset, Cost cost) {
subsets_.push_back(subset);
cost_ += cost;
}
bool Empty() const { return subsets_.empty(); }
void Clear() {
cost_ = 0.0;
subsets_.clear();
}
private:
Cost cost_ = std::numeric_limits<Cost>::max();
std::vector<FullSubsetIndex> subsets_;
};
// In the narrow scope of the CFT subgradient, there are often divisions
// between non-negative quantities (e.g., to compute a relative gap). In these
// specific cases, the denominator should always be greater than the
// numerator. This function checks that.
inline Cost DivideIfGE0(Cost numerator, Cost denominator) {
DCHECK_GE(numerator, -1e-6);
if (numerator < 1e-6) {
return 0.0;
}
return numerator / denominator;
}
// Dual information related to a SubModel.
// Stores multipliers, reduced costs, and the lower bound, and provides an
// interface that keeps them alligned.
class DualState {
public:
DualState() = default;
DualState(const DualState&) = default;
template <typename SubModelT>
explicit DualState(const SubModelT& model)
: lower_bound_(.0),
multipliers_(model.num_elements(), .0),
reduced_costs_(model.subset_costs().begin(),
model.subset_costs().end()) {}
Cost lower_bound() const { return lower_bound_; }
const ElementCostVector& multipliers() const { return multipliers_; }
const SubsetCostVector& reduced_costs() const { return reduced_costs_; }
// NOTE: This function contains one of the two O(nnz) subgradient steps
template <typename SubModelT, typename Op>
void DualUpdate(const SubModelT& model, Op multiplier_operator) {
multipliers_.resize(model.num_elements());
reduced_costs_.resize(model.num_subsets());
lower_bound_ = .0;
// Update multipliers
for (ElementIndex i : model.ElementRange()) {
multiplier_operator(i, multipliers_[i]);
lower_bound_ += multipliers_[i];
DCHECK(std::isfinite(multipliers_[i]));
DCHECK_GE(multipliers_[i], .0);
}
lower_bound_ += ComputeReducedCosts(model, multipliers_, reduced_costs_);
}
private:
// Single hot point to optimize once for the different use cases.
template <typename ModelT>
static Cost ComputeReducedCosts(const ModelT& model,
const ElementCostVector& multipliers,
SubsetCostVector& reduced_costs) {
// Compute new reduced costs (O(nnz))
Cost negative_sum = .0;
for (SubsetIndex j : model.SubsetRange()) {
reduced_costs[j] = model.subset_costs()[j];
for (ElementIndex i : model.columns()[j]) {
reduced_costs[j] -= multipliers[i];
}
if (reduced_costs[j] < .0) {
negative_sum += reduced_costs[j];
}
}
return negative_sum;
}
Cost lower_bound_;
ElementCostVector multipliers_;
SubsetCostVector reduced_costs_;
};
// Utility aggregate to store and pass around both primal and dual states.
struct PrimalDualState {
Solution solution;
DualState dual_state;
};
///////////////////////////////////////////////////////////////////////
///////////////////////////// SUBGRADIENT /////////////////////////////
///////////////////////////////////////////////////////////////////////
// Utilitiy aggregate used by the SubgradientOptimization procedure to
// communicate pass the needed information to the SubgradientCBs interface.
struct SubgradientContext {
const SubModel& model;
const DualState& current_dual_state;
// Avoid copying unused reduced cost during subgradient
const Cost& best_lower_bound;
const ElementCostVector& best_multipliers;
const Solution& best_solution;
const ElementCostVector& subgradient;
};
// Generic set of callbacks hooks used to specialized the behavior of the
// subgradient optimization
class SubgradientCBs {
public:
virtual bool ExitCondition(const SubgradientContext&) = 0;
virtual void RunHeuristic(const SubgradientContext&, Solution&) = 0;
virtual void ComputeMultipliersDelta(const SubgradientContext&,
ElementCostVector& delta_mults) = 0;
virtual bool UpdateCoreModel(SubgradientContext context,
CoreModel& core_model, bool force = false) = 0;
virtual ~SubgradientCBs() = default;
};
// Subgradient optimization procedure. Optimizes the Lagrangian relaxation of
// the Set-Covering problem until a termination criterion si met.
void SubgradientOptimization(SubModel& core_model, SubgradientCBs& cbs,
PrimalDualState& best_state);
// Subgradient callbacks implementation focused on improving the current best
// dual bound.
class BoundCBs : public SubgradientCBs {
public:
static constexpr Cost kTol = 1e-6;
BoundCBs(const SubModel& model);
Cost step_size() const { return step_size_; }
bool ExitCondition(const SubgradientContext& context) override;
void ComputeMultipliersDelta(const SubgradientContext& context,
ElementCostVector& delta_mults) override;
void RunHeuristic(const SubgradientContext& context,
Solution& solution) override {}
bool UpdateCoreModel(SubgradientContext context, CoreModel& core_model,
bool force = false) override;
private:
void MakeMinimalCoverageSubgradient(const SubgradientContext& context,
ElementCostVector& subgradient);
private:
Cost squared_norm_;
ElementCostVector direction_;
std::vector<SubsetIndex> lagrangian_solution_;
// Stopping condition
Cost prev_best_lb_;
BaseInt max_iter_countdown_;
BaseInt exit_test_countdown_;
BaseInt exit_test_period_;
BaseInt unfixed_run_extension_;
// Step size
void UpdateStepSize(SubgradientContext context);
Cost step_size_;
Cost last_min_lb_seen_;
Cost last_max_lb_seen_;
BaseInt step_size_update_countdown_;
BaseInt step_size_update_period_;
};
////////////////////////////////////////////////////////////////////////
/////////////////////// MULTIPLIERS BASED GREEDY ///////////////////////
////////////////////////////////////////////////////////////////////////
// Higher level function that return a function obtained by the dual multiplier
// based greedy.
Solution RunMultiplierBasedGreedy(
const SubModel& model, const DualState& dual_state,
Cost cost_cutoff = std::numeric_limits<BaseInt>::max());
// Lower level greedy function which creates or completes a "solution" (seen as
// set of subsets of the current SubModel) until a cutoff size or cost is
// reached.
// Note: since the cutoff might be reached, the returned solution might not be
// feasible.
Cost CoverGreedly(const SubModel& model, const DualState& dual_state,
Cost cost_cutoff, BaseInt size_cutoff,
std::vector<SubsetIndex>& sol_subsets);
///////////////////////////////////////////////////////////////////////
//////////////////////// THREE PHASE ALGORITHM ////////////////////////
///////////////////////////////////////////////////////////////////////
// Subgradient callbacks implementation focused on wandering near the optimal
// multipliers and invoke the multipliers based greedy heuristic at each
// iteration.
class HeuristicCBs : public SubgradientCBs {
public:
HeuristicCBs() : step_size_(0.1), countdown_(250) {};
void set_step_size(Cost step_size) { step_size_ = step_size; }
bool ExitCondition(const SubgradientContext& context) override {
Cost upper_bound =
context.best_solution.cost() - context.model.fixed_cost();
Cost lower_bound = context.best_lower_bound;
return upper_bound - .999 < lower_bound || --countdown_ <= 0;
}
void RunHeuristic(const SubgradientContext& context,
Solution& solution) override;
void ComputeMultipliersDelta(const SubgradientContext& context,
ElementCostVector& delta_mults) override;
bool UpdateCoreModel(SubgradientContext context, CoreModel& core_model,
bool force = false) override {
return false;
}
private:
Cost step_size_;
BaseInt countdown_;
};
PrimalDualState RunThreePhase(SubModel& model,
const Solution& init_solution = {});
///////////////////////////////////////////////////////////////////////
///////////////////// OUTER REFINEMENT PROCEDURE //////////////////////
///////////////////////////////////////////////////////////////////////
PrimalDualState RunCftHeuristic(SubModel& model,
const Solution& init_solution = {});
///////////////////////////////////////////////////////////////////////
//////////////////////// FULL TO CORE PRICING /////////////////////////
///////////////////////////////////////////////////////////////////////
// Coverage counter to decide the number of columns to keep in the core model.
static constexpr BaseInt kMinCov = 5;
// CoreModel extractor. Stores a pointer to the full model and specilized
// UpdateCore in such a way to updated the SubModel (stored as base class) and
// focus the search on a small windows of the full model.
class FullToCoreModel : public SubModel {
using base = SubModel;
struct UpdateTrigger {
BaseInt countdown;
BaseInt period;
BaseInt max_period;
};
// This class handles the logic for selecting columns based on their reduced
// costs and the number of rows they cover. While this implementation is more
// complex than what would typically be required for static `SetCoverModel`s,
// it is designed to efficiently handle dynamically updated models where new
// columns are generated over time. In this case, recomputing the row view
// from scratch each time would introduce significant overhead. To avoid this,
// the column selection logic operates solely on the column view, without
// relying on the row view.
//
// NOTE: A cleaner alternative would involve modifying the `SetCoverModel`
// implementation to support incremental updates to the row view as new
// columns are added. This approach would reduce overhead while enabling a
// simpler and more efficient column selection process.
//
// NOTE: The row-view based approach is available at commit:
// a598cf83930629853f72b964ebcff01f7a9378e0
class ColumnSelector {
public:
const std::vector<FullSubsetIndex>& ComputeNewSelection(
FilterModelView full_model,
const std::vector<FullSubsetIndex>& forced_columns,
const SubsetCostVector& reduced_costs);
private:
bool SelectColumn(FilterModelView full_model, SubsetIndex j);
void SelecteMinRedCostColumns(FilterModelView full_model,
const SubsetCostVector& reduced_costs);
void SelectMinRedCostByRow(FilterModelView full_model,
const SubsetCostVector& reduced_costs);
private:
std::vector<SubsetIndex> candidates_;
std::vector<SubsetIndex>::const_iterator first_unselected_;
ElementToIntVector row_cover_counts_;
BaseInt rows_left_to_cover_;
std::vector<FullSubsetIndex> selection_;
SubsetBoolVector selected_;
};
public:
FullToCoreModel() = default;
FullToCoreModel(const Model* full_model);
Cost FixMoreColumns(const std::vector<SubsetIndex>& columns_to_fix) override;
void ResetColumnFixing(const std::vector<FullSubsetIndex>& columns_to_fix,
const DualState& state) override;
bool UpdateCore(Cost best_lower_bound,
const ElementCostVector& best_multipliers,
const Solution& best_solution, bool force) override;
void ResetPricingPeriod();
const DualState& best_dual_state() const { return best_dual_state_; }
bool FullToSubModelInvariantCheck();
protected:
void SizeUpdate();
bool IsTimeToUpdate(Cost best_lower_bound, bool force);
decltype(auto) IsFocusCol(FullSubsetIndex j) {
return is_focus_col_[static_cast<SubsetIndex>(j)];
}
decltype(auto) IsFocusRow(FullElementIndex i) {
return is_focus_row_[static_cast<ElementIndex>(i)];
}
void UpdatePricingPeriod(const DualState& full_dual_state,
Cost core_lower_bound, Cost core_upper_bound);
Cost UpdateMultipliers(const ElementCostVector& core_multipliers);
void ComputeAndSetFocus(Cost best_lower_bound, const Solution& best_solution);
// Views are not composable (for now), so we can either access the full_model
// with the strongly typed view or with the filtered view.
// Access the full model filtered by the current columns fixed.
FilterModelView FixingFullModelView() const {
return FilterModelView(full_model_, &is_focus_col_, &is_focus_row_,
full_model_->num_subsets(),
full_model_->num_elements());
}
// Access the full model with the strongly typed view.
StrongModelView StrongTypedFullModelView() const {
return StrongModelView(full_model_);
}
std::vector<FullSubsetIndex> SelectNewCoreColumns(
const std::vector<FullSubsetIndex>& forced_columns = {});
private:
const Model* full_model_;
// Note: The `is_focus_col_` vector duplicates information already present in
// `SubModelView::cols_sizes_`. However, it does not overlap with any data
// stored in `CoreModel`. Since `CoreModel` is expected to be the primary use
// case, this vector is explicitly maintained here to ensure compatibility.
SubsetBoolVector is_focus_col_;
// Note: The `is_focus_row_` vector is functionally redundant with either
// `CoreModel::full2core_row_map_` or `SubModelView::rows_sizes_`. These
// existing structures could be used to create the filtered view of the full
// model. However, doing so would require generalizing the current view
// system to work with generic functors instead of vectors of integral types.
// Since the number of elements is assumed to be not prohibitive, a simpler
// implementation that avoids this memory optimization was preferred.
ElementBoolVector is_focus_row_;
BaseInt selection_coefficient_ = kMinCov;
Cost prev_best_lower_bound_;
DualState full_dual_state_;
DualState best_dual_state_;
BaseInt update_countdown_;
BaseInt update_period_;
BaseInt update_max_period_;
ColumnSelector col_selector_; // Here to avoid reallocations
};
} // namespace operations_research::scp
#endif /* ORTOOLS_SET_COVER_SET_COVER_CFT_H */