From f69287e53ca6076dfdbde9d9bfe036f75f546777 Mon Sep 17 00:00:00 2001 From: Francesco Cavaliere Date: Thu, 1 May 2025 18:28:43 +0200 Subject: [PATCH] Refactored `FullToCoreModel` to simplify extention with column generation --- ortools/set_cover/set_cover_cft.cc | 221 +++++++++++++---------------- ortools/set_cover/set_cover_cft.h | 54 ++++++- 2 files changed, 145 insertions(+), 130 deletions(-) diff --git a/ortools/set_cover/set_cover_cft.cc b/ortools/set_cover/set_cover_cft.cc index 31ca2e671e..88af8d42d5 100644 --- a/ortools/set_cover/set_cover_cft.cc +++ b/ortools/set_cover/set_cover_cft.cc @@ -1004,113 +1004,87 @@ std::vector ComputeTentativeFocus(StrongModelView full_model) { absl::c_sort(columns_focus); return columns_focus; } +} // namespace -// 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: - ColumnSelector(FilterModelView full_model, - const std::vector& forced_columns, - const SubsetCostVector& reduced_costs) - : full_model_(full_model), - candidates_(), - row_cover_counts_(full_model.num_elements(), 0), - rows_left_to_cover_(full_model.num_elements()), - selection_(), - selected_(full_model.num_subsets(), false), - reduced_costs_(reduced_costs) { - // Always retain best solution in the core model (if possible) - for (FullSubsetIndex full_j : forced_columns) { - SubsetIndex j = static_cast(full_j); - if (full_model_.IsFocusCol(j)) { - SelectColumn(j); - } +const std::vector& +FullToCoreModel::ColumnSelector::ComputeNewSelection( + FilterModelView full_model, + const std::vector& forced_columns, + const SubsetCostVector& reduced_costs) { + selected_.assign(full_model.num_subsets(), false); + row_cover_counts_.assign(full_model.num_elements(), 0); + rows_left_to_cover_ = full_model.num_focus_elements(); + selection_.clear(); + candidates_.clear(); + + // Always retain best solution in the core model (if possible) + for (FullSubsetIndex full_j : forced_columns) { + SubsetIndex j = static_cast(full_j); + if (full_model.IsFocusCol(j)) { + SelectColumn(full_model, j); } + } - auto subset_range = full_model.SubsetRange(); - candidates_ = {subset_range.begin(), subset_range.end()}; - absl::c_sort(candidates_, [&](auto j1, auto j2) { - return reduced_costs[j1] < reduced_costs[j2]; - }); - first_unselected_ = candidates_.begin(); - }; + auto subset_range = full_model.SubsetRange(); + candidates_ = {subset_range.begin(), subset_range.end()}; + absl::c_sort(candidates_, [&](auto j1, auto j2) { + return reduced_costs[j1] < reduced_costs[j2]; + }); + first_unselected_ = candidates_.begin(); - bool SelectColumn(SubsetIndex j) { + SelecteMinRedCostColumns(full_model, reduced_costs); + SelectMinRedCostByRow(full_model, reduced_costs); + absl::c_sort(selection_); + return selection_; +} + +bool FullToCoreModel::ColumnSelector::SelectColumn(FilterModelView full_model, + SubsetIndex j) { + if (selected_[j]) { + return false; + } + for (ElementIndex i : full_model.columns()[j]) { + selected_[j] = true; // Detect empty columns + if (++row_cover_counts_[i] == kMinCov) { + --rows_left_to_cover_; + } + } + if (selected_[j]) { // Skip empty comlumns + selection_.push_back(static_cast(j)); + } + return selected_[j]; +} + +void FullToCoreModel::ColumnSelector::SelecteMinRedCostColumns( + FilterModelView full_model, const SubsetCostVector& reduced_costs) { + BaseInt selected_size = 0; + BaseInt max_size = kMinCov * full_model.num_elements(); + auto it = first_unselected_; + while (it != candidates_.end() && reduced_costs[*it] < 0.1 && + selected_size < max_size) { + selected_size += SelectColumn(full_model, *it++) ? 1 : 0; + } + first_unselected_ = it; +} + +void FullToCoreModel::ColumnSelector::SelectMinRedCostByRow( + FilterModelView full_model, const SubsetCostVector& reduced_costs) { + auto it = first_unselected_; + while (it != candidates_.end() && rows_left_to_cover_ > 0) { + SubsetIndex j = *it++; if (selected_[j]) { - return false; + continue; } - for (ElementIndex i : full_model_.columns()[j]) { - selected_[j] = true; // Detect empty columns - if (++row_cover_counts_[i] == kMinCov) { - --rows_left_to_cover_; - } + for (ElementIndex i : full_model.columns()[j]) { + ++row_cover_counts_[i]; + rows_left_to_cover_ += (row_cover_counts_[i] == kMinCov ? 1 : 0); + selected_[j] = selected_[j] || (row_cover_counts_[i] <= kMinCov); } - if (selected_[j]) { // Skip empty comlumns + if (selected_[j]) { selection_.push_back(static_cast(j)); } - return selected_[j]; } - - void SelecteMinRedCostColumns() { - BaseInt selected_size = 0; - BaseInt max_size = kMinCov * full_model_.num_elements(); - auto it = first_unselected_; - while (it != candidates_.end() && reduced_costs_[*it] < 0.1 && - selected_size < max_size) { - selected_size += SelectColumn(*it++) ? 1 : 0; - } - first_unselected_ = it; - } - - void SelectMinRedCostByRow() { - auto it = first_unselected_; - while (it != candidates_.end() && rows_left_to_cover_ > 0) { - SubsetIndex j = *it++; - if (selected_[j]) { - continue; - } - for (ElementIndex i : full_model_.columns()[j]) { - ++row_cover_counts_[i]; - rows_left_to_cover_ += (row_cover_counts_[i] == kMinCov ? 1 : 0); - selected_[j] = selected_[j] || (row_cover_counts_[i] <= kMinCov); - } - if (selected_[j]) { - selection_.push_back(static_cast(j)); - } - } - } - - void SortSelection() { absl::c_sort(selection_); } - - const std::vector& selection() const { return selection_; } - - private: - FilterModelView full_model_; - std::vector candidates_; - std::vector::const_iterator first_unselected_; - ElementToIntVector row_cover_counts_; - BaseInt rows_left_to_cover_; - - std::vector selection_; - SubsetBoolVector selected_; - const SubsetCostVector& reduced_costs_; -}; - -} // namespace +} FullToCoreModel::FullToCoreModel(const Model* full_model) : SubModel(full_model, ComputeTentativeFocus(StrongModelView(full_model))), @@ -1176,15 +1150,10 @@ void FullToCoreModel::ResetColumnFixing( // set the new one while also updating the column focus. This solution is much // simpler. It just create a new core-model object from scratch and then uses // the existing interface. - ColumnSelector col_selector(FixingFullModelView(), full_columns_to_fix, - full_dual_state_.reduced_costs()); - col_selector.SelecteMinRedCostColumns(); - col_selector.SelectMinRedCostByRow(); - col_selector.SortSelection(); - - // Create a new SubModel object from scratch and then fix columns - static_cast(*this) = - SubModel(full_model_, col_selector.selection()); + const auto& selection = col_selector_.ComputeNewSelection( + FixingFullModelView(), full_columns_to_fix, + full_dual_state_.reduced_costs()); + static_cast(*this) = SubModel(full_model_, selection); // TODO(anyone): Improve this. It's Inefficient but hardly a botleneck and it // also avoid storing a full->core column map. @@ -1206,13 +1175,7 @@ void FullToCoreModel::SizeUpdate() { is_focus_col_.resize(full_model_->num_subsets(), true); } -bool FullToCoreModel::UpdateCore(Cost best_lower_bound, - const ElementCostVector& best_multipliers, - const Solution& best_solution, bool force) { - SizeUpdate(); - if (num_focus_subsets() == FixingFullModelView().num_focus_subsets()) { - return false; - } +bool FullToCoreModel::IsTimeToUpdate(Cost best_lower_bound, bool force) { if (!force && --update_countdown_ > 0) { return false; } @@ -1220,23 +1183,31 @@ bool FullToCoreModel::UpdateCore(Cost best_lower_bound, return false; } prev_best_lower_bound_ = best_lower_bound; + return true; +} - UpdateMultipliers(best_multipliers, full_dual_state_, best_dual_state_); - ColumnSelector new_core_columns(FixingFullModelView(), - best_solution.subsets(), - full_dual_state_.reduced_costs()); - new_core_columns.SelecteMinRedCostColumns(); - new_core_columns.SelectMinRedCostByRow(); - new_core_columns.SortSelection(); - SetFocus(new_core_columns.selection()); - +void FullToCoreModel::ComputeAndSetFocus(Cost best_lower_bound, + const Solution& best_solution) { + const auto& selection = col_selector_.ComputeNewSelection( + FixingFullModelView(), best_solution.subsets(), + full_dual_state_.reduced_costs()); + base::SetFocus(selection); UpdatePricingPeriod(full_dual_state_, best_lower_bound, best_solution.cost() - fixed_cost()); VLOG(3) << "[F2CU] Core-update: Lower bounds: real " << full_dual_state_.lower_bound() << ", core " << best_lower_bound << ", core size: " << num_focus_elements() << "x" << num_focus_subsets(); +} +bool FullToCoreModel::UpdateCore(Cost best_lower_bound, + const ElementCostVector& best_multipliers, + const Solution& best_solution, bool force) { + if (!IsTimeToUpdate(best_lower_bound, force)) { + return false; + } + UpdateMultipliers(best_multipliers); + ComputeAndSetFocus(best_lower_bound, best_solution); DCHECK(FullToSubModelInvariantCheck()); return true; } @@ -1261,9 +1232,8 @@ void FullToCoreModel::UpdatePricingPeriod(const DualState& full_dual_state, update_countdown_ = update_period_; } -void FullToCoreModel::UpdateMultipliers( - const ElementCostVector& core_multipliers, DualState& full_dual_state, - DualState& best_dual_state) { +Cost FullToCoreModel::UpdateMultipliers( + const ElementCostVector& core_multipliers) { auto fixing_full_model = FixingFullModelView(); full_dual_state_.DualUpdate( fixing_full_model, [&](ElementIndex full_i, Cost& i_mult) { @@ -1287,6 +1257,7 @@ void FullToCoreModel::UpdateMultipliers( full_dual_state_.lower_bound() > best_dual_state_.lower_bound()) { best_dual_state_ = full_dual_state_; } + return full_dual_state_.lower_bound(); } bool FullToCoreModel::FullToSubModelInvariantCheck() { diff --git a/ortools/set_cover/set_cover_cft.h b/ortools/set_cover/set_cover_cft.h index 691da50984..9481161a57 100644 --- a/ortools/set_cover/set_cover_cft.h +++ b/ortools/set_cover/set_cover_cft.h @@ -336,6 +336,46 @@ class FullToCoreModel : public SubModel { 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& ComputeNewSelection( + FilterModelView full_model, + const std::vector& 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 candidates_; + std::vector::const_iterator first_unselected_; + ElementToIntVector row_cover_counts_; + BaseInt rows_left_to_cover_; + + std::vector selection_; + SubsetBoolVector selected_; + }; + public: FullToCoreModel() = default; FullToCoreModel(const Model* full_model); @@ -348,9 +388,11 @@ class FullToCoreModel : public SubModel { void ResetPricingPeriod(); const DualState& best_dual_state() const { return best_dual_state_; } bool FullToSubModelInvariantCheck(); - void SizeUpdate(); - private: + protected: + void SizeUpdate(); + bool IsTimeToUpdate(Cost best_lower_bound, bool force); + decltype(auto) IsFocusCol(FullSubsetIndex j) { return is_focus_col_[static_cast(j)]; } @@ -359,9 +401,8 @@ class FullToCoreModel : public SubModel { } void UpdatePricingPeriod(const DualState& full_dual_state, Cost core_lower_bound, Cost core_upper_bound); - void UpdateMultipliers(const ElementCostVector& core_multipliers, - DualState& full_dual_state, - DualState& best_dual_state); + 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. @@ -381,6 +422,7 @@ class FullToCoreModel : public SubModel { std::vector SelectNewCoreColumns( const std::vector& forced_columns = {}); + private: const Model* full_model_; // Note: The `is_focus_col_` vector duplicates information already present in @@ -406,6 +448,8 @@ class FullToCoreModel : public SubModel { BaseInt update_countdown_; BaseInt update_period_; BaseInt update_max_period_; + + ColumnSelector col_selector_; // Here to avoid reallocations }; } // namespace operations_research::scp