Files
ortools-clone/ortools/algorithms/set_cover.cc
Corentin Le Molgat 2649b4284a Add int128.proto
2023-12-08 14:28:53 +01:00

389 lines
14 KiB
C++

// Copyright 2010-2022 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.
#include "ortools/algorithms/set_cover.h"
#include <algorithm>
#include <cstddef>
#include <iterator>
#include <limits>
#include <numeric>
#include <vector>
#include "absl/container/flat_hash_set.h"
#include "absl/log/check.h"
#include "absl/random/random.h"
#include "ortools/algorithms/set_cover_ledger.h"
#include "ortools/algorithms/set_cover_model.h"
#include "ortools/algorithms/set_cover_utils.h"
#include "ortools/base/logging.h"
namespace operations_research {
constexpr SubsetIndex kNotFound(-1);
// TrivialSolutionGenerator.
bool TrivialSolutionGenerator::NextSolution() {
return NextSolution(ledger_->model()->all_subsets());
}
bool TrivialSolutionGenerator::NextSolution(
const std::vector<SubsetIndex>& focus) {
const SubsetIndex num_subsets(ledger_->model()->num_subsets());
SubsetBoolVector choices(num_subsets, false);
for (const SubsetIndex subset : focus) {
choices[subset] = true;
}
ledger_->LoadSolution(choices);
return true;
}
// RandomSolutionGenerator.
bool RandomSolutionGenerator::NextSolution() {
return NextSolution(ledger_->model()->all_subsets());
}
bool RandomSolutionGenerator::NextSolution(
const std::vector<SubsetIndex>& focus) {
std::vector<SubsetIndex> shuffled = focus;
std::shuffle(shuffled.begin(), shuffled.end(), absl::BitGen());
for (const SubsetIndex subset : shuffled) {
if (ledger_->is_selected()[subset]) continue;
if (ledger_->marginal_impacts()[subset] != 0) {
ledger_->Toggle(subset, true);
}
}
DCHECK(ledger_->CheckConsistency());
return true;
}
// GreedySolutionGenerator.
void GreedySolutionGenerator::UpdatePriorities(
const std::vector<SubsetIndex>& impacted_subsets) {
const SubsetCostVector& subset_costs = ledger_->model()->subset_costs();
for (const SubsetIndex subset : impacted_subsets) {
const ElementIndex marginal_impact(ledger_->marginal_impacts()[subset]);
if (marginal_impact != 0) {
const Cost marginal_cost_increase =
subset_costs[subset] / marginal_impact.value();
pq_.ChangePriority(subset, -marginal_cost_increase);
} else {
pq_.Remove(subset);
}
}
}
bool GreedySolutionGenerator::NextSolution() {
return NextSolution(ledger_->model()->all_subsets());
}
bool GreedySolutionGenerator::NextSolution(
const std::vector<SubsetIndex>& focus) {
const SubsetCostVector& subset_costs = ledger_->model()->subset_costs();
ledger_->MakeDataConsistent();
// The priority is the minimum marginal cost increase. Since the
// priority queue returns the smallest value, we use the opposite.
for (const SubsetIndex subset : focus) {
if (!ledger_->is_selected()[subset] &&
ledger_->marginal_impacts()[subset] != 0) {
const Cost marginal_cost_increase =
subset_costs[subset] / ledger_->marginal_impacts()[subset].value();
pq_.Add(subset, -marginal_cost_increase);
}
}
const ElementIndex num_elements(ledger_->model()->num_elements());
ElementIndex num_elements_covered(ledger_->num_elements_covered());
while (num_elements_covered < num_elements && !pq_.IsEmpty()) {
const SubsetIndex best_subset = pq_.TopSubset();
DVLOG(1) << "Best subset: " << best_subset.value()
<< " Priority = " << pq_.Priority(best_subset)
<< " queue size = " << pq_.Size();
const std::vector<SubsetIndex> impacted_subsets =
ledger_->Toggle(best_subset, true);
UpdatePriorities(impacted_subsets);
num_elements_covered = ledger_->num_elements_covered();
DVLOG(1) << "Cost = " << ledger_->cost() << " num_uncovered_elements = "
<< num_elements - num_elements_covered;
}
DCHECK(pq_.IsEmpty());
DCHECK(ledger_->CheckConsistency());
DCHECK(ledger_->CheckSolution());
return true;
}
// SteepestSearch.
void SteepestSearch::UpdatePriorities(
const std::vector<SubsetIndex>& impacted_subsets) {
// Update priority queue. Since best_subset is in impacted_subsets, it will
// be removed.
for (const SubsetIndex subset : impacted_subsets) {
pq_.Remove(subset);
}
}
bool SteepestSearch::NextSolution(int num_iterations) {
return NextSolution(ledger_->model()->all_subsets(), num_iterations);
}
bool SteepestSearch::NextSolution(const std::vector<SubsetIndex>& focus,
int num_iterations) {
// Return false if ledger_ contains no solution.
if (!ledger_->CheckSolution()) return false;
const SubsetCostVector& subset_costs = ledger_->model()->subset_costs();
// Create priority queue with cost of using a subset, by decreasing order.
// Do it only for removable subsets.
for (const SubsetIndex subset : focus) {
// The priority is the gain from removing the subset from the solution.
if (ledger_->is_selected()[subset] && ledger_->is_removable()[subset]) {
pq_.Add(subset, subset_costs[subset]);
}
}
for (int iteration = 0; iteration < num_iterations && !pq_.IsEmpty();
++iteration) {
const SubsetIndex best_subset = pq_.TopSubset();
const Cost cost_decrease = subset_costs[best_subset];
DCHECK_GT(cost_decrease, 0.0);
DCHECK(ledger_->is_removable()[best_subset]);
DCHECK(ledger_->is_selected()[best_subset]);
const std::vector<SubsetIndex> impacted_subsets =
ledger_->Toggle(best_subset, false);
UpdatePriorities(impacted_subsets);
DVLOG(1) << "Cost = " << ledger_->cost();
}
DCHECK(ledger_->CheckConsistency());
DCHECK(ledger_->CheckSolution());
return true;
}
// Guided Tabu Search
void GuidedTabuSearch::Initialize() {
const SparseColumnView& columns = ledger_->model()->columns();
const SubsetCostVector& subset_costs = ledger_->model()->subset_costs();
times_penalized_.AssignToZero(columns.size());
augmented_costs_ = subset_costs;
utilities_ = subset_costs;
}
namespace {
bool FlipCoin() {
// TODO(user): use STL for repeatable testing.
return absl::Bernoulli(absl::BitGen(), 0.5);
}
} // namespace
void GuidedTabuSearch::UpdatePenalties(const std::vector<SubsetIndex>& focus) {
const SubsetCostVector& subset_costs = ledger_->model()->subset_costs();
Cost max_utility = -1.0;
for (const SubsetIndex subset : focus) {
if (ledger_->is_selected()[subset]) {
max_utility = std::max(max_utility, utilities_[subset]);
}
}
const double epsilon_utility = epsilon_ * max_utility;
for (const SubsetIndex subset : focus) {
if (ledger_->is_selected()[subset]) {
const double utility = utilities_[subset];
if ((max_utility - utility <= epsilon_utility) && FlipCoin()) {
++times_penalized_[subset];
const int times_penalized = times_penalized_[subset];
const Cost cost =
subset_costs[subset]; // / columns[subset].size().value();
utilities_[subset] = cost / (1 + times_penalized);
augmented_costs_[subset] =
cost * (1 + penalty_factor_ * times_penalized);
}
}
}
}
bool GuidedTabuSearch::NextSolution(int num_iterations) {
return NextSolution(ledger_->model()->all_subsets(), num_iterations);
}
bool GuidedTabuSearch::NextSolution(const std::vector<SubsetIndex>& focus,
int num_iterations) {
const SubsetCostVector& subset_costs = ledger_->model()->subset_costs();
constexpr Cost kMaxPossibleCost = std::numeric_limits<Cost>::max();
Cost best_cost = ledger_->cost();
SubsetBoolVector best_choices = ledger_->is_selected();
Cost augmented_cost =
std::accumulate(augmented_costs_.begin(), augmented_costs_.end(), 0.0);
for (int iteration = 0; iteration < num_iterations; ++iteration) {
Cost best_delta = kMaxPossibleCost;
SubsetIndex best_subset = kNotFound;
for (const SubsetIndex subset : focus) {
const Cost delta = augmented_costs_[subset];
DVLOG(1) << "Subset, " << subset.value() << ", at ,"
<< ledger_->is_selected()[subset] << ", is removable =, "
<< ledger_->is_removable()[subset] << ", delta =, " << delta
<< ", best_delta =, " << best_delta;
if (ledger_->is_selected()[subset]) {
// Try to remove subset from solution, if the gain from removing is
// worth it:
if (-delta < best_delta &&
// and it can be removed, and
ledger_->is_removable()[subset] &&
// it is not Tabu OR decreases the actual cost (aspiration):
(!tabu_list_.Contains(subset) ||
ledger_->cost() - subset_costs[subset] < best_cost)) {
best_delta = -delta;
best_subset = subset;
}
} else {
// Try to use subset in solution, if its penalized delta is good.
if (delta < best_delta) {
// The limit kMaxPossibleCost is ill-defined,
// there is always a best_subset. Is it intended?
if (!tabu_list_.Contains(subset)) {
best_delta = delta;
best_subset = subset;
}
}
}
}
if (best_subset == kNotFound) { // Local minimum reached.
ledger_->LoadSolution(best_choices);
return true;
}
DVLOG(1) << "Best subset, " << best_subset.value() << ", at ,"
<< ledger_->is_selected()[best_subset] << ", is removable = ,"
<< ledger_->is_removable()[best_subset] << ", best_delta = ,"
<< best_delta;
UpdatePenalties(focus);
tabu_list_.Add(best_subset);
const std::vector<SubsetIndex> impacted_subsets = ledger_->UnsafeToggle(
best_subset, !ledger_->is_selected()[best_subset]);
// TODO(user): make the cost computation incremental.
augmented_cost =
std::accumulate(augmented_costs_.begin(), augmented_costs_.end(), 0.0);
DVLOG(1) << "Iteration, " << iteration << ", current cost = ,"
<< ledger_->cost() << ", best cost = ," << best_cost
<< ", penalized cost = ," << augmented_cost;
if (ledger_->cost() < best_cost) {
LOG(INFO) << "Updated best cost, "
<< "Iteration, " << iteration << ", current cost = ,"
<< ledger_->cost() << ", best cost = ," << best_cost
<< ", penalized cost = ," << augmented_cost;
best_cost = ledger_->cost();
best_choices = ledger_->is_selected();
}
}
ledger_->LoadSolution(best_choices);
DCHECK(ledger_->CheckConsistency());
DCHECK(ledger_->CheckSolution());
return true;
}
namespace {
void SampleSubsets(std::vector<SubsetIndex>* list, std::size_t num_subsets) {
num_subsets = std::min(num_subsets, list->size());
CHECK_GE(num_subsets, 0);
std::shuffle(list->begin(), list->end(), absl::BitGen());
list->resize(num_subsets);
}
} // namespace
std::vector<SubsetIndex> ClearRandomSubsets(std::size_t num_subsets,
SetCoverLedger* ledger) {
return ClearRandomSubsets(ledger->model()->all_subsets(), num_subsets,
ledger);
}
std::vector<SubsetIndex> ClearRandomSubsets(
const std::vector<SubsetIndex>& focus, std::size_t num_subsets,
SetCoverLedger* ledger) {
num_subsets = std::min(num_subsets, focus.size());
CHECK_GE(num_subsets, 0);
std::vector<SubsetIndex> chosen_indices;
for (const SubsetIndex subset : focus) {
if (ledger->is_selected()[subset]) {
chosen_indices.push_back(subset);
}
}
SampleSubsets(&chosen_indices, num_subsets);
for (const SubsetIndex subset : chosen_indices) {
// Use UnsafeToggle because we allow non-solutions.
ledger->UnsafeToggle(subset, false);
}
return chosen_indices;
}
std::vector<SubsetIndex> ClearMostCoveredElements(std::size_t num_subsets,
SetCoverLedger* ledger) {
return ClearMostCoveredElements(ledger->model()->all_subsets(), num_subsets,
ledger);
}
std::vector<SubsetIndex> ClearMostCoveredElements(
const std::vector<SubsetIndex>& focus, std::size_t num_subsets,
SetCoverLedger* ledger) {
// This is the vector we will return.
std::vector<SubsetIndex> chosen_indices;
const ElementToSubsetVector& coverage = ledger->coverage();
// Compute a permutation of the element indices by decreasing order of
// coverage by element.
std::vector<ElementIndex> permutation(coverage.size().value());
std::iota(permutation.begin(), permutation.end(), 0);
std::sort(permutation.begin(), permutation.end(),
[&coverage](ElementIndex i, ElementIndex j) {
return coverage[i] > coverage[j];
});
// Now, for the elements that are over-covered (coverage > 1), collect the
// sets that are used.
absl::flat_hash_set<SubsetIndex> used_subsets_collection;
for (ElementIndex element : permutation) {
if (coverage[element] <= 1) break;
for (SubsetIndex subset : ledger->model()->rows()[element]) {
if (ledger->is_selected()[subset]) {
used_subsets_collection.insert(subset);
}
}
}
// Now the impacted subset is a vector representation of the flat_hash_set
// collection.
std::vector<SubsetIndex> impacted_subsets(used_subsets_collection.begin(),
used_subsets_collection.end());
// Sort the impacted subsets to be able to intersect the vector later.
std::sort(impacted_subsets.begin(), impacted_subsets.end());
// chosen_indices = focus ⋂ impacted_subsets
std::set_intersection(focus.begin(), focus.end(), impacted_subsets.begin(),
impacted_subsets.end(),
std::back_inserter(chosen_indices));
std::shuffle(chosen_indices.begin(), chosen_indices.end(), absl::BitGen());
chosen_indices.resize(std::min(chosen_indices.size(), num_subsets));
// Sort before traversing indices (and memory) in order.
std::sort(chosen_indices.begin(), chosen_indices.end());
for (const SubsetIndex subset : chosen_indices) {
// Use UnsafeToggle because we allow non-solutions.
ledger->UnsafeToggle(subset, false);
}
return chosen_indices;
}
} // namespace operations_research