From 040bb0ca5f11d2d5ee019c7fef7d02741f7e71ad Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Wed, 24 Apr 2024 14:34:27 +0200 Subject: [PATCH] more work on set covering --- ortools/algorithms/set_cover_heuristics.cc | 136 ++++++------ ortools/algorithms/set_cover_invariant.cc | 246 ++++++++++++--------- ortools/algorithms/set_cover_invariant.h | 72 +++--- 3 files changed, 240 insertions(+), 214 deletions(-) diff --git a/ortools/algorithms/set_cover_heuristics.cc b/ortools/algorithms/set_cover_heuristics.cc index c43e15937d..047bab3a63 100644 --- a/ortools/algorithms/set_cover_heuristics.cc +++ b/ortools/algorithms/set_cover_heuristics.cc @@ -83,14 +83,37 @@ bool RandomSolutionGenerator::NextSolution( std::shuffle(shuffled.begin(), shuffled.end(), absl::BitGen()); for (const SubsetIndex subset : shuffled) { if (inv_->is_selected()[subset]) continue; - if (inv_->marginal_impacts()[subset] != 0) { - inv_->Toggle(subset, true); + if (inv_->num_free_elements()[subset] != 0) { + inv_->UnsafeUse(subset); } } DCHECK(inv_->CheckConsistency()); return true; } +namespace { +bool LogAndCheck(const SetCoverInvariant* inv, + const std::vector& focus) { + for (const SubsetIndex subset : focus) { + DVLOG(1) << "subset: " << subset.value() + << " is_selected: " << inv->is_selected()[subset] + << " is_removable: " << inv->is_removable()[subset] + << " cardinality: " << inv->model()->columns()[subset].size() + << " marginal_impact: " << inv->num_free_elements()[subset] + << " num overcovered elements: " + << inv->model()->columns()[subset].size().value() - + inv->num_coverage_le_1_elements()[subset].value(); + } + DCHECK(inv->CheckConsistency()); + for (const SubsetIndex subset : focus) { + DCHECK_EQ(inv->is_removable()[subset], + inv->num_coverage_le_1_elements()[subset] == 0); + } + DCHECK_EQ(inv->num_uncovered_elements(), 0); + return true; +} +} // anonymous namespace + // GreedySolutionGenerator. bool GreedySolutionGenerator::NextSolution() { @@ -114,10 +137,11 @@ bool GreedySolutionGenerator::NextSolution( DVLOG(1) << "focus.size(): " << focus.size(); subset_priorities.reserve(focus.size()); for (const SubsetIndex subset : focus) { - if (!inv_->is_selected()[subset] && inv_->marginal_impacts()[subset] != 0) { + if (!inv_->is_selected()[subset] && + inv_->num_free_elements()[subset] != 0) { // NOMUTANTS -- reason, for C++ const float priority = - elements_per_cost[subset] * inv_->marginal_impacts()[subset].value(); + elements_per_cost[subset] * inv_->num_free_elements()[subset].value(); subset_priorities.push_back({priority, subset.value()}); } } @@ -126,15 +150,14 @@ bool GreedySolutionGenerator::NextSolution( // TODO(user): research more about the best value for Arity. AdjustableKAryHeap pq( subset_priorities, inv_->model()->num_subsets().value()); - const ElementIndex num_elements(inv_->model()->num_elements()); - ElementIndex num_elements_covered(inv_->num_elements_covered()); - while (num_elements_covered < num_elements && !pq.IsEmpty()) { + while (!pq.IsEmpty()) { const SubsetIndex best_subset(pq.Pop().index()); const std::vector impacted_subsets = - inv_->Toggle(best_subset, true); + inv_->UnsafeUse(best_subset); + // NOMUTANTS -- reason, for C++ + if (inv_->num_uncovered_elements() == 0) break; for (const SubsetIndex subset : impacted_subsets) { - if (subset == best_subset) continue; - const ElementIndex marginal_impact(inv_->marginal_impacts()[subset]); + const ElementIndex marginal_impact(inv_->num_free_elements()[subset]); if (marginal_impact > 0) { const float priority = marginal_impact.value() * elements_per_cost[subset]; @@ -145,45 +168,26 @@ bool GreedySolutionGenerator::NextSolution( } for (const SubsetIndex subset : focus) { DCHECK_EQ(inv_->is_removable()[subset], - inv_->num_overcovered_elements()[subset].value() == - inv_->model()->columns()[subset].size().value()); + inv_->num_coverage_le_1_elements()[subset] == 0); } - - num_elements_covered = inv_->num_elements_covered(); - DVLOG(1) << "Cost = " << inv_->cost() << " num_uncovered_elements = " - << num_elements - num_elements_covered; + DVLOG(1) << "Cost = " << inv_->cost() + << " num_uncovered_elements = " << inv_->num_uncovered_elements(); } - DCHECK(pq.IsEmpty()); - DCHECK(num_elements_covered == num_elements); - DCHECK(inv_->CheckConsistency()); - for (const SubsetIndex subset : focus) { - DVLOG(1) << "subset: " << subset.value() - << " is_selected: " << inv_->is_selected()[subset] - << " is_removable: " << inv_->is_removable()[subset] - << " cardinality: " << inv_->model()->columns()[subset].size() - << " marginal_impact: " << inv_->marginal_impacts()[subset] - << " num overcovered elements: " - << inv_->num_overcovered_elements()[subset]; - } - for (const SubsetIndex subset : focus) { - DCHECK_EQ(inv_->is_removable()[subset], - inv_->num_overcovered_elements()[subset].value() == - inv_->model()->columns()[subset].size().value()); - } - + // Don't expect the queue to be empty, because of the break in the while loop. + DCHECK(LogAndCheck(inv_, focus)); return true; } -class SubsetIndexWithElementDegree { +class ElementIndexWithDegree { public: using Index = int; using Priority = int; - SubsetIndexWithElementDegree() = default; - SubsetIndexWithElementDegree(Priority priority, Index index) + ElementIndexWithDegree() = default; + ElementIndexWithDegree(Priority priority, Index index) : index_(index), priority_(priority) {} Priority priority() const { return priority_; } Index index() const { return index_; } - inline bool operator<(const SubsetIndexWithElementDegree other) const { + inline bool operator<(const ElementIndexWithDegree other) const { if (other.priority() != priority()) { return priority() < other.priority(); } @@ -210,7 +214,7 @@ bool ElementDegreeSolutionGenerator::NextSolution( DVLOG(1) << "Entering ElementDegreeSolutionGenerator::NextSolution"; inv_->RecomputeInvariant(); const ElementIndex num_elements(inv_->model()->num_elements()); - std::vector element_degrees; + std::vector element_degrees; element_degrees.reserve(num_elements.value()); const SparseRowView& rows = inv_->model()->rows(); for (ElementIndex element(0); element < num_elements; ++element) { @@ -222,54 +226,39 @@ bool ElementDegreeSolutionGenerator::NextSolution( // The priority queue maintains the non-covered element with the smallest // degree. - AdjustableKAryHeap pq( + AdjustableKAryHeap pq( element_degrees, num_elements.value()); - ElementIndex num_elements_covered(inv_->num_elements_covered()); const SparseColumnView& columns = inv_->model()->columns(); - while (num_elements_covered < num_elements && !pq.IsEmpty()) { + while (!pq.IsEmpty()) { const ElementIndex best_element(pq.Pop().index()); DCHECK_EQ(inv_->coverage()[best_element], 0) << "Element " << best_element.value() - << " is already covered. num_elements_covered: " << num_elements_covered + << " is already covered. num_uncovered_elements: " + << inv_->num_uncovered_elements() << " / num_elements: " << num_elements; Cost min_ratio = std::numeric_limits::max(); SubsetIndex best_subset(-1); for (const SubsetIndex subset : rows[best_element]) { const Cost ratio = - costs[subset] / inv_->marginal_impacts()[subset].value(); + costs[subset] / inv_->num_free_elements()[subset].value(); if (ratio < min_ratio) { min_ratio = ratio; best_subset = subset; } } DCHECK_NE(best_subset, -1); - inv_->Toggle(best_subset, true); + inv_->UnsafeUse(best_subset); + if (inv_->num_uncovered_elements() == 0) break; for (const ElementIndex element : columns[best_subset]) { if (element != best_element) { pq.Remove(element.value()); } } - num_elements_covered = inv_->num_elements_covered(); - DVLOG(1) << "Cost = " << inv_->cost() << " num_uncovered_elements = " - << num_elements - num_elements_covered; - } - DCHECK(pq.IsEmpty()); - DCHECK(num_elements_covered == num_elements); - DCHECK(inv_->CheckConsistency()); - for (const SubsetIndex subset : focus) { - DVLOG(1) << "subset: " << subset.value() - << " is_selected: " << inv_->is_selected()[subset] - << " is_removable: " << inv_->is_removable()[subset] - << " cardinality: " << inv_->model()->columns()[subset].size() - << " marginal_impact: " << inv_->marginal_impacts()[subset] - << " num overcovered elements: " - << inv_->num_overcovered_elements()[subset]; - } - for (const SubsetIndex subset : focus) { - DCHECK_EQ(inv_->is_removable()[subset], - inv_->num_overcovered_elements()[subset].value() == - inv_->model()->columns()[subset].size().value()); + DVLOG(1) << "Cost = " << inv_->cost() + << " num_uncovered_elements = " << inv_->num_uncovered_elements(); } + // Don't expect the queue to be empty, because of the break in the while loop. + DCHECK(LogAndCheck(inv_, focus)); return true; } @@ -293,7 +282,7 @@ bool SteepestSearch::NextSolution(absl::Span focus, DVLOG(1) << "Entering SteepestSearch::NextSolution, num_iterations = " << num_iterations; // Return false if inv_ contains no solution. - if (inv_->num_elements_covered() != inv_->model()->num_elements()) { + if (inv_->num_uncovered_elements() != 0) { return false; } @@ -316,7 +305,7 @@ bool SteepestSearch::NextSolution(absl::Span focus, if (!inv_->is_removable()[best_subset]) continue; DCHECK_GT(costs[best_subset], 0.0); const std::vector impacted_subsets = - inv_->Toggle(best_subset, false); + inv_->UnsafeRemove(best_subset); for (const SubsetIndex subset : impacted_subsets) { if (!inv_->is_removable()[subset]) { pq.Remove(subset.value()); @@ -324,7 +313,7 @@ bool SteepestSearch::NextSolution(absl::Span focus, } DVLOG(1) << "Cost = " << inv_->cost(); } - DCHECK(inv_->num_elements_covered() == inv_->model()->num_elements()); + DCHECK_EQ(inv_->num_uncovered_elements(), 0); DCHECK(inv_->CheckConsistency()); return true; } @@ -447,7 +436,8 @@ bool GuidedTabuSearch::NextSolution(const std::vector& focus, } } inv_->LoadSolution(best_choices); - DCHECK(inv_->num_elements_covered() == inv_->model()->num_elements()); + DCHECK_EQ(inv_->num_uncovered_elements(), 0); + DCHECK(inv_->CheckConsistency()); return true; } @@ -478,8 +468,8 @@ std::vector ClearRandomSubsets(absl::Span focus, } SampleSubsets(&chosen_indices, num_subsets); for (const SubsetIndex subset : chosen_indices) { - // Use UnsafeToggle because we allow non-solutions. - inv->UnsafeToggle(subset, false); + // We can use UnsafeRemove because we allow non-solutions. + inv->UnsafeRemove(subset); } return chosen_indices; } @@ -530,8 +520,8 @@ std::vector ClearMostCoveredElements( // Testing has shown that sorting sampled_subsets is not necessary. // Now, un-select the subset in sampled_subsets. for (const SubsetIndex subset : sampled_subsets) { - // Use UnsafeToggle because we allow non-solutions. - inv->UnsafeToggle(subset, false); + // Use UnsafeRemove because we allow non-solutions. + inv->UnsafeRemove(subset); } return sampled_subsets; } diff --git a/ortools/algorithms/set_cover_invariant.cc b/ortools/algorithms/set_cover_invariant.cc index 6368f62fbb..c61a4cee01 100644 --- a/ortools/algorithms/set_cover_invariant.cc +++ b/ortools/algorithms/set_cover_invariant.cc @@ -25,26 +25,29 @@ namespace operations_research { // Note: in many of the member functions, variables have "crypterse" names // to avoid confusing them with member data. For example mrgnl_impcts is used -// to avoid confusion with marginal_impacts_. +// to avoid confusion with num_free_elements_. void SetCoverInvariant::Initialize() { DCHECK(model_->ComputeFeasibility()); model_->CreateSparseRowView(); + cost_ = 0.0; + const SubsetIndex num_subsets(model_->num_subsets()); is_selected_.assign(num_subsets, false); is_removable_.assign(num_subsets, false); - marginal_impacts_.assign(num_subsets, ElementIndex(0)); - num_overcovered_elements_.assign(num_subsets, ElementIndex(0)); + num_free_elements_.assign(num_subsets, ElementIndex(0)); + num_coverage_le_1_elements_.assign(num_subsets, ElementIndex(0)); const SparseColumnView& columns = model_->columns(); for (SubsetIndex subset(0); subset < num_subsets; ++subset) { - marginal_impacts_[subset] = columns[subset].size().value(); + num_free_elements_[subset] = columns[subset].size().value(); + num_coverage_le_1_elements_[subset] = columns[subset].size().value(); } const ElementIndex num_elements(model_->num_elements()); coverage_.assign(num_elements, SubsetIndex(0)); - cost_ = 0.0; - num_elements_covered_ = ElementIndex(0); + + num_uncovered_elements_ = num_elements; } bool SetCoverInvariant::CheckConsistency() const { @@ -53,16 +56,13 @@ bool SetCoverInvariant::CheckConsistency() const { for (ElementIndex element(0); element < model_->num_elements(); ++element) { CHECK_EQ(cvrg[element], coverage_[element]); } - auto [num_elts_cvrd, mrgnl_impcts, num_ovrcvrd_elts, is_rmvble] = + auto [num_uncvrd_elts, mrgnl_impcts, num_01_elts, is_rdndnt] = ComputeImpliedData(cvrg); - CHECK_EQ(num_elts_cvrd, num_elements_covered_); const SparseColumnView& columns = model_->columns(); for (SubsetIndex subset(0); subset < columns.size(); ++subset) { - CHECK_EQ(mrgnl_impcts[subset], marginal_impacts_[subset]); - CHECK_EQ(num_ovrcvrd_elts[subset], num_overcovered_elements_[subset]); - CHECK_EQ(is_rmvble[subset], is_removable_[subset]); - CHECK_EQ(is_rmvble[subset], - num_ovrcvrd_elts[subset] == columns[subset].size().value()); + CHECK_EQ(mrgnl_impcts[subset], num_free_elements_[subset]); + CHECK_EQ(is_rdndnt[subset], is_removable_[subset]); + CHECK_EQ(is_rdndnt[subset], num_01_elts[subset] == 0); } return true; } @@ -74,9 +74,10 @@ void SetCoverInvariant::LoadSolution(const SubsetBoolVector& c) { void SetCoverInvariant::RecomputeInvariant() { std::tie(cost_, coverage_) = ComputeCostAndCoverage(is_selected_); - std::tie(num_elements_covered_, marginal_impacts_, num_overcovered_elements_, - is_removable_) = ComputeImpliedData(coverage_); - } + std::tie(num_uncovered_elements_, num_free_elements_, + num_coverage_le_1_elements_, is_removable_) = + ComputeImpliedData(coverage_); +} std::tuple SetCoverInvariant::ComputeCostAndCoverage( @@ -84,7 +85,7 @@ SetCoverInvariant::ComputeCostAndCoverage( Cost cst = 0.0; ElementToSubsetVector cvrg(model_->num_elements(), SubsetIndex(0)); const SparseColumnView& columns = model_->columns(); - // Initialize marginal_impacts_, update cost_, and compute the coverage for + // Initialize coverage, update cost, and compute the coverage for // all the elements covered by the selected subsets. const SubsetCostVector& subset_costs = model_->subset_costs(); for (SubsetIndex subset(0); bool b : choices) { @@ -99,71 +100,91 @@ SetCoverInvariant::ComputeCostAndCoverage( return {cst, cvrg}; } -std::tuple +std::tuple // Removability for each subset SetCoverInvariant::ComputeImpliedData(const ElementToSubsetVector& cvrg) const { - ElementIndex num_elts_cvrd(0); - SubsetIndex num_subsets = model_->num_subsets(); - SubsetToElementVector mrgnl_impcts(num_subsets, ElementIndex(0)); + const ElementIndex num_elements(model_->num_elements()); + ElementIndex num_uncvrd_elts(num_elements); + + const SubsetIndex num_subsets(model_->num_subsets()); SubsetToElementVector num_ovrcvrd_elts(num_subsets, ElementIndex(0)); - SubsetBoolVector is_rmvble(num_subsets, false); + SubsetToElementVector num_free_elts(num_subsets, ElementIndex(0)); + SubsetToElementVector num_cvrg_le_1_elts(num_subsets, ElementIndex(0)); + SubsetBoolVector is_rdndnt(num_subsets, false); const SparseColumnView& columns = model_->columns(); - // Initialize marginal_impacts_. + // Initialize number of free elements and number of elements covered 0 or 1. for (SubsetIndex subset(0); subset < num_subsets; ++subset) { - mrgnl_impcts[subset] = columns[subset].size().value(); -} + num_free_elts[subset] = columns[subset].size().value(); + num_cvrg_le_1_elts[subset] = columns[subset].size().value(); + } // Update num_ovrcvrd_elts[subset] and num_elts_cvrd. // When all elements in subset are overcovered, is_removable_[subset] is true. const SparseRowView& rows = model_->rows(); - const ElementIndex num_elements(model_->num_elements()); for (ElementIndex element(0); element < num_elements; ++element) { if (cvrg[element] == 1) { - ++num_elts_cvrd; // Just need to update this ... - for (SubsetIndex subset : rows[element]) { - --mrgnl_impcts[subset]; // ... and marginal_impacts_. + --num_uncvrd_elts; // Just need to update this ... + for (SubsetIndex subset : rows[element]) { + --num_free_elts[subset]; } } else { if (cvrg[element] >= 2) { - ++num_elts_cvrd; // Same as when cvrg[element] == 1 + --num_uncvrd_elts; // Same as when cvrg[element] == 1 for (SubsetIndex subset : rows[element]) { - --mrgnl_impcts[subset]; // Same as when cvrg[element] >= 2 - ++num_ovrcvrd_elts[subset]; - if (num_ovrcvrd_elts[subset] == columns[subset].size().value()) { - is_rmvble[subset] = true; + --num_free_elts[subset]; // Same as when cvrg[element] == 1 + --num_cvrg_le_1_elts[subset]; + if (num_cvrg_le_1_elts[subset] == 0) { + is_rdndnt[subset] = true; + } + } + } } } -} - } -} - return {num_elts_cvrd, mrgnl_impcts, num_ovrcvrd_elts, is_rmvble}; + return {num_uncvrd_elts, num_free_elts, num_cvrg_le_1_elts, is_rdndnt}; } std::vector SetCoverInvariant::Toggle(SubsetIndex subset, bool value) { - // Note: "if p then q" is also "not(p) or q", or p <= q (p LE q). - // If selected, then is_removable, to make sure we still have a solution. - DCHECK(is_selected_[subset] <= is_removable_[subset]) - << "is_selected_ = " << is_selected_[subset] - << " is_removable_ = " << is_removable_[subset] << " subset = " << subset; - // If value, then marginal_impact > 0, to not increase the cost. - DCHECK((value <= (marginal_impacts_[subset] > 0))); - return UnsafeToggle(subset, value); + if (value) { + DCHECK(!is_removable_[subset]); + DCHECK_GT(num_free_elements_[subset], 0); + return UnsafeUse(subset); + } else { + DCHECK(is_removable_[subset]); + return UnsafeRemove(subset); + } } std::vector SetCoverInvariant::UnsafeToggle(SubsetIndex subset, bool value) { - DCHECK_NE(is_selected_[subset], value); - // If already selected, then marginal_impact == 0. - DCHECK(is_selected_[subset] <= (marginal_impacts_[subset] == 0)); - DVLOG(1) << (value ? "S" : "Des") << "electing subset " << subset; - is_selected_[subset] = value; + if (value) { + return UnsafeUse(subset); + } else { + return UnsafeRemove(subset); + } +} + +namespace { +void CollectSubsets(SubsetIndex s, std::vector* subsets, + SubsetBoolVector* subset_seen) { + if (!(*subset_seen)[s]) { + (*subset_seen)[s] = true; + subsets->push_back(s); + } +} +} // namespace + +std::vector SetCoverInvariant::UnsafeUse(SubsetIndex subset) { + DCHECK(!is_selected_[subset]); + DVLOG(1) << "Selecting subset " << subset; + is_selected_[subset] = true; const SubsetCostVector& subset_costs = model_->subset_costs(); - cost_ += value ? subset_costs[subset] : -subset_costs[subset]; - const int delta = value ? 1 : -1; + cost_ += subset_costs[subset]; const SparseColumnView& columns = model_->columns(); @@ -172,66 +193,77 @@ std::vector SetCoverInvariant::UnsafeToggle(SubsetIndex subset, impacted_subsets.reserve(columns.size().value()); // Initialize subset_seen so that `subset` not be in impacted_subsets. - subset_seen[subset] = true; - - auto collect_impacted_subsets = [&impacted_subsets, - &subset_seen](SubsetIndex s) { - if (!subset_seen[s]) { - subset_seen[s] = true; - impacted_subsets.push_back(s); - } - }; + subset_seen[subset] = true; + const SparseRowView& rows = model_->rows(); for (const ElementIndex element : columns[subset]) { // Update coverage. - coverage_[element] += delta; - - // There are four different types of events that matter when updating - // the invariant. Two for each case when value is true or false. - const SparseRowView& rows = model_->rows(); - if (value) { - if (coverage_[element] == 1) { - // `element` is newly covered. - ++num_elements_covered_; - for (const SubsetIndex impacted_subset : rows[element]) { - collect_impacted_subsets(impacted_subset); - --marginal_impacts_[impacted_subset]; + ++coverage_[element]; + if (coverage_[element] == 1) { + // `element` is newly covered. + --num_uncovered_elements_; + for (const SubsetIndex impacted_subset : rows[element]) { + CollectSubsets(impacted_subset, &impacted_subsets, &subset_seen); + --num_free_elements_[impacted_subset]; + } + } else if (coverage_[element] == 2) { + // `element` is newly overcovered. + for (const SubsetIndex impacted_subset : rows[element]) { + CollectSubsets(impacted_subset, &impacted_subsets, &subset_seen); + --num_coverage_le_1_elements_[impacted_subset]; + if (num_coverage_le_1_elements_[impacted_subset] == 0) { + // All the elements in impacted_subset are now overcovered, so it + // is removable. Note that this happens only when the last element + // of impacted_subset becomes overcovered. + is_removable_[impacted_subset] = true; + } } - } else if (coverage_[element] == 2) { - // `element` is newly overcovered. - for (const SubsetIndex impacted_subset : rows[element]) { - collect_impacted_subsets(impacted_subset); - ++num_overcovered_elements_[impacted_subset]; - if (num_overcovered_elements_[impacted_subset] == - columns[impacted_subset].size().value()) { - // All the elements in impacted_subset are now overcovered, so it - // is removable. Note that this happens only when the last element - // of impacted_subset becomes overcovered. - is_removable_[impacted_subset] = true; - } - } - } - } else { // value is false - if (coverage_[element] == 0) { - // `element` is no longer covered. - --num_elements_covered_; - for (const SubsetIndex impacted_subset : rows[element]) { - collect_impacted_subsets(impacted_subset); - ++marginal_impacts_[impacted_subset]; - } - } else if (coverage_[element] == 1) { - // `element` is no longer overcovered. - for (const SubsetIndex impacted_subset : rows[element]) { - collect_impacted_subsets(impacted_subset); - --num_overcovered_elements_[impacted_subset]; - if (num_overcovered_elements_[impacted_subset] == - columns[impacted_subset].size().value() - 1) { - // There is one element of impacted_subset which is not overcovered. - // impacted_subset has just become non-removable. - is_removable_[impacted_subset] = false; } } + DCHECK(CheckConsistency()); + return impacted_subsets; } + +std::vector SetCoverInvariant::UnsafeRemove(SubsetIndex subset) { + DCHECK(is_selected_[subset]); + // If already selected, then num_free_elements == 0. + DCHECK_EQ(num_free_elements_[subset], 0); + DVLOG(1) << "Deselecting subset " << subset; + is_selected_[subset] = false; + const SubsetCostVector& subset_costs = model_->subset_costs(); + cost_ -= subset_costs[subset]; + + const SparseColumnView& columns = model_->columns(); + + SubsetBoolVector subset_seen(columns.size(), false); + std::vector impacted_subsets; + impacted_subsets.reserve(columns.size().value()); + + // Initialize subset_seen so that `subset` not be in impacted_subsets. + subset_seen[subset] = true; + + const SparseRowView& rows = model_->rows(); + for (const ElementIndex element : columns[subset]) { + // Update coverage. + --coverage_[element]; + if (coverage_[element] == 0) { + // `element` is no longer covered. + ++num_uncovered_elements_; + for (const SubsetIndex impacted_subset : rows[element]) { + CollectSubsets(impacted_subset, &impacted_subsets, &subset_seen); + ++num_free_elements_[impacted_subset]; + } + } else if (coverage_[element] == 1) { + // `element` is no longer overcovered. + for (const SubsetIndex impacted_subset : rows[element]) { + CollectSubsets(impacted_subset, &impacted_subsets, &subset_seen); + ++num_coverage_le_1_elements_[impacted_subset]; + if (num_coverage_le_1_elements_[impacted_subset] == 1) { + // There is one element of impacted_subset which is not overcovered. + // impacted_subset has just become non-removable. + is_removable_[impacted_subset] = false; + } + } } } DCHECK(CheckConsistency()); diff --git a/ortools/algorithms/set_cover_invariant.h b/ortools/algorithms/set_cover_invariant.h index a914b5d812..1148756168 100644 --- a/ortools/algorithms/set_cover_invariant.h +++ b/ortools/algorithms/set_cover_invariant.h @@ -37,16 +37,14 @@ using SubsetBoolVector = glop::StrictITIVector; // for an explanation of the terminology. // // A SetCoverInvariant is (relatively) small: -// is_selected_, a partial solution, vector of Booleans of size #subsets. +// is_selected_: a partial solution, vector of Booleans of size #subsets. // From this, the following can be computed: -// coverage_, the number of times an elememt is covered; -// marginal_impacts_, the number of elements of a subset still uncovered; -// num_overcovered_elements_, the number of elements of a subset that -// are covered two times or more in the current solution; +// coverage_: the number of times an elememt is covered; +// num_free_elements_: the number of elements of a subset still uncovered; +// num_coverage_le_1_elements_: the number of elements of a subset that +// are covered 1 time or less (not overcovered) in the current solution; // is_removable_, whether a subset can be removed from the solution. -// is_removable_[subset] == (num_over_cover_elements_[subet] == card(subset)) -// where card(subset) is the size() of the column corresponding to subset in -// the model. +// is_removable_[subset] == (num_coverage_le_1_elements_[subet] == 0). class SetCoverInvariant { public: // Constructs an empty weighted set covering solver state. @@ -71,19 +69,24 @@ class SetCoverInvariant { // Returns the cost of current solution. Cost cost() const { return cost_; } + // Returns the number of uncovered elements. + ElementIndex num_uncovered_elements() const { + return num_uncovered_elements_; + } + // Returns the subset assignment vector. const SubsetBoolVector& is_selected() const { return is_selected_; } // Returns vector containing the number of elements in each subset that are // not covered in the current solution. - const SubsetToElementVector& marginal_impacts() const { - return marginal_impacts_; + const SubsetToElementVector& num_free_elements() const { + return num_free_elements_; } - // Returns vector containing the number of elements in each subset that are - // covered two times or more in the current solution. - const SubsetToElementVector& num_overcovered_elements() const { - return num_overcovered_elements_; + // Returns the vector of numbers of free or exactly covered elements for + // each subset. + const SubsetToElementVector& num_coverage_le_1_elements() const { + return num_coverage_le_1_elements_; } // Returns vector containing number of subsets covering each element. @@ -93,9 +96,6 @@ class SetCoverInvariant { // the solution. const SubsetBoolVector& is_removable() const { return is_removable_; } - // Returns the number of elements covered. - ElementIndex num_elements_covered() const { return num_elements_covered_; } - // Stores the solution and recomputes the data in the invariant. void LoadSolution(const SubsetBoolVector& c); @@ -117,6 +117,10 @@ class SetCoverInvariant { // Only checks that value is different from is_selected_[subset]. std::vector UnsafeToggle(SubsetIndex subset, bool value); + std::vector UnsafeUse(SubsetIndex subset); + + std::vector UnsafeRemove(SubsetIndex subset); + // Returns the current solution as a proto. SetCoverSolutionResponse ExportSolutionAsProto() const; @@ -129,13 +133,12 @@ class SetCoverInvariant { const SubsetBoolVector& choices) const; // Computes the implied data for the given coverage cvrg: - // The implied consists of: - // - the number of elements covered, - // - the vector of marginal impacts for each subset, - // - the vector of overcovered elements for each subset, - // - the vector of removability for each subset. - std::tuple + // The implied data consists of: + std::tuple // Removability for each subset. ComputeImpliedData(const ElementToSubsetVector& cvrg) const; // Internal UnsafeToggle where value is a constant for the template. @@ -148,26 +151,27 @@ class SetCoverInvariant { // Current cost. Cost cost_; - // The number of elements covered in the current solution. - ElementIndex num_elements_covered_; + // The number of uncovered (or free) elements in the current solution. + ElementIndex num_uncovered_elements_; // Current assignment. SubsetBoolVector is_selected_; - // The marginal impact of a subset is the number of elements in that subset - // that are not covered in the current solution. - SubsetToElementVector marginal_impacts_; + // A vector that for each subset gives the number of free elements, i.e. + // elements whose coverage is 0. + // problem. + SubsetToElementVector num_free_elements_; - // Counts the number of "overcovered" elements for a given subset. Overcovered - // elements are the ones whose coverage is 2 and above. - SubsetToElementVector num_overcovered_elements_; + // Counts the number of free or exactly covered elements, i.e. whose coverage + // is 0 or 1. + SubsetToElementVector num_coverage_le_1_elements_; // The coverage of an element is the number of used subsets which contains // the said element. ElementToSubsetVector coverage_; - // True if the subset can be removed from the solution without making it - // infeasible. + // True if the subset is redundant, i.e. can be removed from the solution + // without making it infeasible. SubsetBoolVector is_removable_; };