From 03e2551e6164ad7f06a1fc8e66e638f46793e2fe Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Mon, 21 Jul 2025 17:27:51 +0200 Subject: [PATCH] set_cover: backport from main --- ortools/set_cover/samples/bin_packing.cc | 501 +++++++++++++++++++ ortools/set_cover/samples/bin_packing.h | 180 +++++++ ortools/set_cover/samples/bin_packing_cft.cc | 144 ++++++ ortools/set_cover/samples/set_cover.py | 1 + 4 files changed, 826 insertions(+) create mode 100644 ortools/set_cover/samples/bin_packing.cc create mode 100644 ortools/set_cover/samples/bin_packing.h create mode 100644 ortools/set_cover/samples/bin_packing_cft.cc diff --git a/ortools/set_cover/samples/bin_packing.cc b/ortools/set_cover/samples/bin_packing.cc new file mode 100644 index 0000000000..c89a8ee4db --- /dev/null +++ b/ortools/set_cover/samples/bin_packing.cc @@ -0,0 +1,501 @@ + +// 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. + +#include "ortools/set_cover/samples/bin_packing.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "ortools/base/stl_util.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover_cft.h" +#include "ortools/set_cover/set_cover_submodel.h" +#include "ortools/util/filelineiter.h" + +namespace operations_research { + +void BinPackingModel::set_bin_capacity(Cost capacity) { + if (capacity <= 0) { + LOG(WARNING) << "Bin capacity must be positive."; + return; + } + bin_capacity_ = capacity; +} +void BinPackingModel::AddItem(Cost weight) { + if (weight > bin_capacity_) { + LOG(WARNING) << "Element weight exceeds bin capacity."; + return; + } + weigths_.push_back(weight); + is_sorted_ = false; +} +void BinPackingModel::SortWeights() { + if (!is_sorted_) { + absl::c_sort(weigths_); + is_sorted_ = true; + } +} + +namespace { +template +bool SimpleAtoValue(absl::string_view str, int_type* out) { + return absl::SimpleAtoi(str, out); +} +bool SimpleAtoValue(absl::string_view str, bool* out) { + return absl::SimpleAtob(str, out); +} +bool SimpleAtoValue(absl::string_view str, double* out) { + return absl::SimpleAtod(str, out); +} +bool SimpleAtoValue(absl::string_view str, float* out) { + return absl::SimpleAtof(str, out); +} +} // namespace + +BinPackingModel ReadBpp(absl::string_view filename) { + BinPackingModel model; + BaseInt num_items = 0; + for (const std::string& line : + FileLines(filename, FileLineIterator::REMOVE_INLINE_CR | + FileLineIterator::REMOVE_BLANK_LINES)) { + if (num_items == 0) { + if (!SimpleAtoValue(line, &num_items)) { + LOG(WARNING) << "Invalid number of elements in file: " << line; + } + continue; + } + + Cost value = .0; + if (!SimpleAtoValue(line, &value)) { + LOG(WARNING) << "Invalid value in file: " << line; + continue; + } + if (model.bin_capacity() <= .0) { + DCHECK_GT(value, .0); + model.set_bin_capacity(value); + } else { + model.AddItem(value); + } + } + DCHECK_GT(model.bin_capacity(), .0); + DCHECK_GT(model.weights().size(), .0); + DCHECK_EQ(num_items, model.weights().size()); + model.SortWeights(); + return model; +} + +BinPackingModel ReadCsp(absl::string_view filename) { + BinPackingModel model; + BaseInt num_item_types = 0; + for (const std::string& line : + FileLines(filename, FileLineIterator::REMOVE_INLINE_CR | + FileLineIterator::REMOVE_BLANK_LINES)) { + if (num_item_types == 0) { + if (!SimpleAtoValue(line, &num_item_types)) { + LOG(WARNING) << "Invalid number of elements in file: " << line; + } + continue; + } + if (model.bin_capacity() <= .0) { + Cost capacity = .0; + if (!SimpleAtoValue(line, &capacity)) { + LOG(WARNING) << "Invalid value in file: " << line; + } + model.set_bin_capacity(capacity); + continue; + } + + std::pair weight_and_demand = + absl::StrSplit(line, absl::ByAnyChar(" :\t"), absl::SkipEmpty()); + + Cost weight = .0; + if (!SimpleAtoValue(weight_and_demand.first, &weight)) { + LOG(WARNING) << "Invalid weight in file: " << line; + continue; + } + + BaseInt demand = 0; + if (!SimpleAtoValue(weight_and_demand.second, &demand)) { + LOG(WARNING) << "Invalid demand in file: " << line; + continue; + } + for (BaseInt i = 0; i < demand; ++i) { + model.AddItem(weight); + } + } + DCHECK_GT(model.bin_capacity(), .0); + DCHECK_GT(model.weights().size(), .0); + DCHECK_GE(num_item_types, model.num_items()); + model.SortWeights(); + return model; +} + +void BestFit(const ElementCostVector& weights, Cost bin_capacity, + const std::vector& items, PartialBins& bins_data) { + for (ElementIndex item : items) { + Cost item_weight = weights[item]; + BaseInt selected_bin = bins_data.bins.size(); + for (BaseInt bin = 0; bin < bins_data.bins.size(); ++bin) { + Cost max_load = bin_capacity - item_weight; + if (bins_data.loads[bin] <= max_load && + (selected_bin == bins_data.bins.size() || + bins_data.loads[bin] > bins_data.loads[selected_bin])) { + selected_bin = bin; + } + } + if (selected_bin == bins_data.bins.size()) { + bins_data.bins.emplace_back(); + bins_data.loads.emplace_back(); + } + bins_data.bins[selected_bin].push_back(item); + bins_data.loads[selected_bin] += item_weight; + } +} + +const SparseColumn& BinPackingSetCoverModel::BinPackingModelGlobals::GetBin( + SubsetIndex j) const { + if (j < SubsetIndex(full_model.num_subsets())) { + return full_model.columns()[j]; + } + DCHECK(candidate_bin != nullptr); + return *candidate_bin; +} + +uint64_t BinPackingSetCoverModel::BinHash::operator()(SubsetIndex j) const { + DCHECK(globals != nullptr); + return absl::HashOf(globals->GetBin(j)); +} + +uint64_t BinPackingSetCoverModel::BinEq::operator()(SubsetIndex j1, + SubsetIndex j2) const { + DCHECK(globals != nullptr); + return globals->GetBin(j1) == globals->GetBin(j2); +} + +bool BinPackingSetCoverModel::AddBin(const SparseColumn& bin) { + if (TryInsertBin(bin)) { + globals_.full_model.AddEmptySubset(1.0); + for (ElementIndex i : bin) { + globals_.full_model.AddElementToLastSubset(i); + } + return true; + } + return false; +} + +bool BinPackingSetCoverModel::TryInsertBin(const SparseColumn& bin) { + DCHECK(absl::c_is_sorted(bin)); + DCHECK(absl::c_adjacent_find(bin) == bin.end()); + DCHECK(globals_.candidate_bin == nullptr); + globals_.candidate_bin = &bin; + + SubsetIndex candidate_j(globals_.full_model.num_subsets()); + bool inserted = bin_set_.insert(candidate_j).second; + + globals_.candidate_bin = nullptr; + return inserted; +} + +void InsertBinsIntoModel(PartialBins& bins_data, + BinPackingSetCoverModel& model) { + for (BaseInt i = 0; i < bins_data.bins.size(); ++i) { + if (bins_data.bins[i].size() > 0) { + absl::c_sort(bins_data.bins[i]); + model.AddBin(bins_data.bins[i]); + } + } +} + +void AddRandomizedBins(const BinPackingModel& model, BaseInt num_bins, + BinPackingSetCoverModel& scp_model, std::mt19937& rnd) { + PartialBins bins_data; + std::vector items(model.num_items()); + absl::c_iota(items, ElementIndex(0)); + + while (scp_model.full_model().num_subsets() < num_bins) { + // Generate bins all containing a specific item + for (ElementIndex n : model.ItemRange()) { + BaseInt unique_bin_num = scp_model.full_model().num_subsets(); + VLOG_EVERY_N_SEC(1, 1) + << "[RGEN] Generating bins: " << unique_bin_num << " / " << num_bins + << " (" << 100.0 * unique_bin_num / num_bins << "%)"; + if (scp_model.full_model().num_subsets() >= num_bins) { + break; + } + + absl::c_shuffle(items, rnd); + + auto n_it = absl::c_find(items, n); + std::iter_swap(n_it, items.end() - 1); + items.pop_back(); + + bins_data.bins.clear(); + bins_data.loads.clear(); + for (BaseInt j = 0; j < 10; ++j) { + bins_data.bins.push_back({n}); + bins_data.loads.push_back(model.weights()[n]); + } + BestFit(model.weights(), model.bin_capacity(), items, bins_data); + InsertBinsIntoModel(bins_data, scp_model); + + items.push_back(n); + + if (unique_bin_num == scp_model.full_model().num_subsets()) { + LOG(INFO) << "No new bins generated."; + break; + } + } + } + + scp_model.CompleteModel(); +} + +BinPackingSetCoverModel GenerateInitialBins(const BinPackingModel& model) { + BinPackingSetCoverModel scp_model(&model); + PartialBins bins_data; + std::vector items(model.num_items()); + + absl::c_iota(items, ElementIndex(0)); + absl::c_sort(items, [&](auto i1, auto i2) { + return model.weights()[i1] > model.weights()[i2]; + }); + BestFit(model.weights(), model.bin_capacity(), items, bins_data); + InsertBinsIntoModel(bins_data, scp_model); + VLOG(1) << "[BFIT] Largest first best-fit solution: " << bins_data.bins.size() + << " bins"; + + scp_model.CompleteModel(); + return scp_model; +} + +void ExpKnap::SaveBin() { + collected_bins_.back().clear(); + for (ElementIndex i : exceptions_) { + break_selection_[i] = !break_selection_[i]; + } + for (ElementIndex i : break_solution_) { + if (break_selection_[i]) { + collected_bins_.back().push_back(i); + } + } + for (ElementIndex i : exceptions_) { + if (break_selection_[i]) { + collected_bins_.back().push_back(i); + } + break_selection_[i] = !break_selection_[i]; + } + absl::c_sort(collected_bins_.back()); + DCHECK(absl::c_adjacent_find(collected_bins_.back()) == + collected_bins_.back().end()); +} +void ExpKnap::InitSolver(const ElementCostVector& profits, + const ElementCostVector& weights, Cost capacity, + BaseInt bnb_nodes_limit) { + capacity_ = capacity; + items_.resize(profits.size()); + collected_bins_.clear(); + for (ElementIndex i; i < ElementIndex(items_.size()); ++i) { + items_[i] = {profits[i], weights[i], i}; + } + absl::c_sort(items_, [](Item i1, Item i2) { + return i1.profit / i1.weight < i2.profit / i2.weight; + }); + + bnb_node_countdown_ = bnb_nodes_limit; + best_delta_ = .0; + exceptions_.clear(); + break_selection_.assign(profits.size(), false); + break_solution_.clear(); +} + +void ExpKnap::FindGoodColumns(const ElementCostVector& profits, + const ElementCostVector& weights, Cost capacity, + BaseInt bnb_nodes_limit) { + InitSolver(profits, weights, capacity, bnb_nodes_limit); + Cost curr_best_cost = .0; + PartialBins more_bins; + std::vector remaining_items; + + bnb_node_countdown_ = bnb_nodes_limit; + inserted_items_.assign(profits.size(), false); + do { + collected_bins_.emplace_back(); + Heuristic(); + VLOG(5) << "[KPCG] Heuristic solution: cost " + << break_profit_sum_ + best_delta_; + EleBranch(); + + for (ElementIndex i : collected_bins_.back()) { + inserted_items_[i] = true; + } + gtl::STLEraseAllFromSequenceIf( + &items_, [&](Item item) { return inserted_items_[item.index]; }); + } while (!items_.empty() && break_profit_sum_ + best_delta_ > 1.0); +} + +bool ExpKnap::EleBranch() { + exceptions_.clear(); + return EleBranch(.0, break_weight_sum_ - capacity_, break_it_ - 1, break_it_); +} + +namespace { +static Cost BoundCheck(Cost best_delta, Cost profit_delta, Cost overweight, + ExpKnap::Item item) { + Cost bound = best_delta + 0.0; // + 1.0 for integral profits + return (profit_delta - bound) * item.weight - overweight * item.profit; +} +} // namespace + +// Adapted version from David Pisinger elebranch function: +// https://hjemmesider.diku.dk/~pisinger/expknap.c +bool ExpKnap::EleBranch(Cost profit_delta, Cost overweigth, ItemIt out_item, + ItemIt in_item) { + VLOG(6) << "[KPCG] EleBranch: profit_delta " << profit_delta << " overweigth " + << overweigth; + if (bnb_node_countdown_-- <= 0) { + return false; + } + bool improved = false; + + if (overweigth <= .0) { + if (profit_delta > best_delta_) { + best_delta_ = profit_delta; + improved = true; + VLOG(5) << "[KPCG] Improved best cost " + << break_profit_sum_ + best_delta_; + } + + bool maximal = true; + while (bnb_node_countdown_ > 0 && in_item < items_.rend() && + BoundCheck(best_delta_, profit_delta, overweigth, *in_item) >= 0) { + exceptions_.push_back(in_item->index); + Cost next_delta = profit_delta + in_item->profit; + Cost next_oweight = overweigth + in_item->weight; + maximal &= !EleBranch(next_delta, next_oweight, out_item, ++in_item); + exceptions_.pop_back(); + } + + if (improved && maximal) { + SaveBin(); + } + improved |= !maximal; + } else { + while (bnb_node_countdown_ > 0 && out_item >= items_.rbegin() && + BoundCheck(best_delta_, profit_delta, overweigth, *out_item) >= 0) { + exceptions_.push_back(out_item->index); + Cost next_delta = profit_delta - out_item->profit; + Cost next_oweight = overweigth - out_item->weight; + improved |= EleBranch(next_delta, next_oweight, --out_item, in_item); + exceptions_.pop_back(); + } + } + return improved; +} + +void ExpKnap::Heuristic() { + best_delta_ = break_profit_sum_ = break_weight_sum_ = .0; + break_it_ = items_.rbegin(); + break_selection_.assign(items_.size(), false); + break_solution_.clear(); + exceptions_.clear(); + while (break_it_ < items_.rend() && + break_it_->weight <= capacity_ - break_weight_sum_) { + break_profit_sum_ += break_it_->profit; + break_weight_sum_ += break_it_->weight; + break_selection_[break_it_->index] = true; + break_solution_.push_back(break_it_->index); + ++break_it_; + } + Cost residual = capacity_ - break_weight_sum_; + + VLOG(5) << "[KPCG] Break solution: cost " << break_profit_sum_ + << ", residual " << residual; + + Cost profit_delta_ub = residual * break_it_->profit / break_it_->weight; + if (profit_delta_ub == .0) { + SaveBin(); + return; + } + + // Try filling the residual space with less efficient (maybe smaller) items + best_delta_ = .0; + for (auto it = break_it_; it < items_.rend(); it++) { + if (it->weight <= residual && it->profit > best_delta_) { + exceptions_ = {it->index}; + best_delta_ = it->profit; + if (best_delta_ >= profit_delta_ub) { + SaveBin(); + return; + } + } + } + + // Try removing an item and adding the break item + Cost min_weight = break_it_->weight - residual; + for (auto it = break_it_ - 1; it >= items_.rbegin(); it--) { + Cost profit_delta = break_it_->profit - it->profit; + if (it->weight >= min_weight && profit_delta > best_delta_) { + exceptions_ = {break_it_->index, it->index}; + best_delta_ = profit_delta; + if (best_delta_ >= profit_delta_ub) { + SaveBin(); + return; + } + } + } + SaveBin(); +} + +bool BinPackingSetCoverModel::UpdateCore( + Cost best_lower_bound, const ElementCostVector& best_multipliers, + const scp::Solution& best_solution, bool force) { + if (!base::IsTimeToUpdate(best_lower_bound, force)) { + return false; + } + + Cost full_lower_bound = base::UpdateMultipliers(best_multipliers); + if (scp::DivideIfGE0(std::abs(full_lower_bound - best_lower_bound), + best_lower_bound) < 0.01 && + best_lower_bound != prev_lower_bound_) { + prev_lower_bound_ = best_lower_bound; + knapsack_solver_.FindGoodColumns(best_multipliers, bpp_model_->weights(), + bpp_model_->bin_capacity(), + /*bnb_nodes_limit=*/1000); + BaseInt num_added_bins = 0; + for (const SparseColumn& bin : knapsack_solver_.collected_bins()) { + num_added_bins += AddBin(bin) ? 1 : 0; + } + if (num_added_bins > 0) { + VLOG(4) << "[KPCG] Added " << num_added_bins << " / " + << globals_.full_model.num_subsets() << " bins"; + } + base::SizeUpdate(); + // TODO(user): add incremental update only for the new columns just added + base::UpdateMultipliers(best_multipliers); + } + + if (base::num_focus_subsets() < FixingFullModelView().num_focus_subsets()) { + ComputeAndSetFocus(best_lower_bound, best_solution); + return true; + } + return false; +} +} // namespace operations_research diff --git a/ortools/set_cover/samples/bin_packing.h b/ortools/set_cover/samples/bin_packing.h new file mode 100644 index 0000000000..5bdba77135 --- /dev/null +++ b/ortools/set_cover/samples/bin_packing.h @@ -0,0 +1,180 @@ + +// 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 OR_TOOLS_ORTOOLS_SET_COVER_SAMPLES_BIN_PACKING_H +#define OR_TOOLS_ORTOOLS_SET_COVER_SAMPLES_BIN_PACKING_H + +#include +#include +#include +#include +#include + +#include +#include + +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover_cft.h" + +namespace operations_research { + +class BinPackingModel { + public: + BinPackingModel() = default; + BaseInt num_items() const { return weigths_.size(); } + Cost bin_capacity() const { return bin_capacity_; } + void set_bin_capacity(Cost capacity); + const ElementCostVector& weights() const { return weigths_; } + void AddItem(Cost weight); + void SortWeights(); + ElementRange ItemRange() const { + return {ElementIndex(), ElementIndex(weigths_.size())}; + } + + private: + bool is_sorted_ = false; + Cost bin_capacity_ = .0; + ElementCostVector weigths_ = {}; +}; + +struct PartialBins { + std::vector bins; + std::vector loads; +}; + +using SubsetHashVector = util_intops::StrongVector; + +class ExpKnap { + public: + struct Item { + Cost profit; // profit + Cost weight; // weight + ElementIndex index; + }; + using ItemIt = std::vector::const_reverse_iterator; + + void SaveBin(); + void FindGoodColumns(const ElementCostVector& profits, + const ElementCostVector& weights, Cost capacity, + BaseInt bnb_nodes_limit); + void InitSolver(const ElementCostVector& profits, + const ElementCostVector& weights, Cost capacity, + BaseInt bnb_nodes_limit); + + bool EleBranch(); + bool EleBranch(Cost profit_sum, Cost overweight, ItemIt out_item, + ItemIt in_item); + + void Heuristic(); + + const std::vector& collected_bins() const { + return collected_bins_; + } + Cost best_cost() const { return break_profit_sum_ + best_delta_; } + + private: + Cost capacity_; // capacity + util_intops::StrongVector items_; // items + ItemIt break_it_; + Cost break_profit_sum_; + Cost break_weight_sum_; + Cost best_delta_; + std::vector exceptions_; + std::vector collected_bins_; + ElementBoolVector break_selection_; + ElementBoolVector inserted_items_; + SparseColumn break_solution_; + BaseInt bnb_node_countdown_; + std::mt19937 rnd_; +}; + +class BinPackingSetCoverModel : public scp::FullToCoreModel { + using base = scp::FullToCoreModel; + + struct BinPackingModelGlobals { + // Dirty hack to avoid invalidation of pointers/references + // A pointer to this data structure is used to compute the hash of bins + // starting from their indices. + scp::Model full_model; + + // External bins do not have a valid index in the model, a temporary pointer + // is used insteda (even dirtier hack). + const SparseColumn* candidate_bin; + + const SparseColumn& GetBin(SubsetIndex j) const; + }; + + struct BinHash { + const BinPackingModelGlobals* globals; + uint64_t operator()(SubsetIndex j) const; + }; + + struct BinEq { + const BinPackingModelGlobals* globals; + uint64_t operator()(SubsetIndex j1, SubsetIndex j2) const; + }; + + public: + BinPackingSetCoverModel(const BinPackingModel* bpp_model) + : globals_{scp::Model(), nullptr}, + bpp_model_(bpp_model), + knapsack_solver_(), + bin_set_({}, 0, BinHash{&globals_}, BinEq{&globals_}), + column_gen_countdown_(10), + column_gen_period_(10) {} + const scp::Model& full_model() const { return globals_.full_model; } + + bool AddBin(const SparseColumn& bin); + void CompleteModel() { + globals_.full_model.CreateSparseRowView(); + static_cast(*this) = + scp::FullToCoreModel(&globals_.full_model); + } + + bool UpdateCore(Cost best_lower_bound, + const ElementCostVector& best_multipliers, + const scp::Solution& best_solution, bool force) override; + + private: + bool TryInsertBin(const SparseColumn& bin); + + BinPackingModelGlobals globals_; + const BinPackingModel* bpp_model_; + ExpKnap knapsack_solver_; + + // Contains bin indices, but it really should contains "bins" (aka, + // SparseColumn). However to avoid redundant allocations (in scp::Model and + // in the set) we cannot store them also here. We cannot also use + // iterators/pointers/references because they can be invalidated. So we store + // bin indices and do ungodly hacky shenanigans to get the bins from them. + absl::flat_hash_set bin_set_; + + Cost prev_lower_bound_; + BaseInt column_gen_countdown_; + BaseInt column_gen_period_; +}; + +BinPackingModel ReadBpp(absl::string_view filename); +BinPackingModel ReadCsp(absl::string_view filename); + +void BestFit(const BinPackingModel& model, + const std::vector& items, PartialBins& bins_data); + +BinPackingSetCoverModel GenerateInitialBins(const BinPackingModel& model); + +void AddRandomizedBins(const BinPackingModel& model, BaseInt num_bins, + BinPackingSetCoverModel& scp_model, std::mt19937& rnd); + +} // namespace operations_research +#endif /* OR_TOOLS_ORTOOLS_SET_COVER_SAMPLES_BIN_PACKING_H */ diff --git a/ortools/set_cover/samples/bin_packing_cft.cc b/ortools/set_cover/samples/bin_packing_cft.cc new file mode 100644 index 0000000000..4a311d71e1 --- /dev/null +++ b/ortools/set_cover/samples/bin_packing_cft.cc @@ -0,0 +1,144 @@ + +// 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. + +#include +#include + +#include +#include + +#include "ortools/base/init_google.h" +#include "ortools/set_cover/base_types.h" +#include "ortools/set_cover/set_cover_cft.h" +#include "ortools/set_cover/samples/bin_packing.h" + +using namespace operations_research; +ABSL_FLAG(std::string, instance, "", "BPP instance int RAIL format."); +ABSL_FLAG(int, bins, 1000, "Number of bins to generate."); + +template +std::string Stringify(const Iterable& col) { + std::string result; + for (auto i : col) { + absl::StrAppend(&result, " ", i); + } + return result; +} + +bool operator==(const SparseColumn& lhs, const std::vector& rhs) { + if (lhs.size() != rhs.size()) return false; + auto lit = lhs.begin(); + auto rit = rhs.begin(); + while (lit != lhs.end() && rit != rhs.end()) { + if (static_cast(*lit) != *rit) { + return false; + } + ++lit; + ++rit; + } + return true; +} + +void RunTest(const ElementCostVector& weights, const ElementCostVector& profits, + const std::vector& expected) { + ExpKnap knap_solver; + + for (ElementIndex i; i < ElementIndex(weights.size()); ++i) { + std::cout << "Item " << i << " -- profit: " << profits[i] + << " weight: " << weights[i] + << " efficiency: " << profits[i] / weights[i] << "\n"; + } + + knap_solver.InitSolver(profits, weights, 6, 100000000); + knap_solver.Heuristic(); + std::cout << "Heur solution cost " << knap_solver.best_cost() << " -- " + << Stringify(knap_solver.collected_bins()[0]) << "\n"; + + knap_solver.EleBranch(); + std::cout << "B&b solution cost " << knap_solver.best_cost() << " -- " + << Stringify(knap_solver.collected_bins()[0]) << "\n"; + + const auto& result = knap_solver.collected_bins()[0]; + if (!(result == expected)) { + std::cout << "Error: expected " << Stringify(expected) << " but got " + << Stringify(result) << "\n"; + } + std::cout << std::endl; +} + +void KnapsackTest() { + std::cout << "Testing knapsack\n"; + ExpKnap knap_solver; + ElementCostVector ws = {1, 2, 3, 4, 5}; + RunTest(ws, {10, 20, 30, 40, 51}, {0, 4}); + RunTest(ws, {10, 20, 30, 41, 50}, {1, 3}); + RunTest(ws, {10, 20, 31, 40, 50}, {0, 1, 2}); + RunTest(ws, {10, 21, 30, 41, 50}, {1, 3}); + RunTest(ws, {11, 21, 30, 40, 50}, {0, 1, 2}); + RunTest(ws, {11, 20, 31, 40, 50}, {0, 1, 2}); + RunTest(ws, {11, 20, 30, 41, 50}, {0, 4}); + RunTest(ws, {11, 20, 30, 40, 51}, {0, 4}); + RunTest(ws, {11, 21, 31, 40, 50}, {0, 1, 2}); + RunTest({4.1, 2, 2, 2}, {8.5, 3, 3, 3}, {1, 2, 3}); +} + +int main(int argc, char** argv) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + // KnapsackTest(); + // return 0; + + BinPackingModel model = ReadBpp(absl::GetFlag(FLAGS_instance)); + + // Quick run with a minimal set of bins + BinPackingSetCoverModel scp_model = GenerateInitialBins(model); + scp::PrimalDualState best_result = scp::RunCftHeuristic(scp_model); + + if (absl::GetFlag(FLAGS_bins) > 0) { + // Run the CFT again with more bins to get a better solution + std::mt19937 rnd(0); + AddRandomizedBins(model, absl::GetFlag(FLAGS_bins), scp_model, rnd); + scp::PrimalDualState result = + scp::RunCftHeuristic(scp_model, best_result.solution); + if (result.solution.cost() < best_result.solution.cost()) { + best_result = result; + } + } + + auto [solution, dual] = best_result; + if (solution.subsets().empty()) { + std::cerr << "Error: failed to find any solution\n"; + } else { + std::cout << "Solution: " << solution.cost() << '\n'; + } + + if (dual.multipliers().empty()) { + std::cerr << "Error: failed to find any dual\n"; + } else { + std::cout << "Core Lower bound: " << dual.lower_bound() << '\n'; + } + + // The lower bound computed on the full model is not a real lower bound unless + // the knapsack subproblem failed to fined any negative reduced cost bin to + // add to the set cover model. + // TODO(anyone): add a flag to indicate if a valid LB has been found or not. + if (scp_model.best_dual_state().multipliers().empty()) { + std::cerr << "Error: no real dual state has been computed\n"; + } else { + std::cout << "Restricted Lower bound: " + << scp_model.best_dual_state().lower_bound() << '\n'; + } + + return EXIT_SUCCESS; +} diff --git a/ortools/set_cover/samples/set_cover.py b/ortools/set_cover/samples/set_cover.py index c6d5b48aef..fd6e8f7055 100755 --- a/ortools/set_cover/samples/set_cover.py +++ b/ortools/set_cover/samples/set_cover.py @@ -17,6 +17,7 @@ # [START program] # [START import] from ortools.set_cover.python import set_cover + # [END import]