Skip to content

Commit 5529e2a

Browse files
committed
Fixed some problems with BPP column generation
1 parent d74962d commit 5529e2a

2 files changed

Lines changed: 135 additions & 87 deletions

File tree

ortools/algorithms/bin_packing.cc

Lines changed: 114 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
#include <absl/strings/string_view.h>
2222

2323
#include <algorithm>
24+
#include <iterator>
2425
#include <random>
2526

27+
#include "ortools/base/stl_util.h"
2628
#include "ortools/set_cover/base_types.h"
2729
#include "ortools/set_cover/set_cover_cft.h"
2830
#include "ortools/set_cover/set_cover_submodel.h"
@@ -146,13 +148,13 @@ BinPackingModel ReadCsp(absl::string_view filename) {
146148
return model;
147149
}
148150

149-
void BestFit(const BinPackingModel& model,
151+
void BestFit(const ElementCostVector& weights, Cost bin_capacity,
150152
const std::vector<ElementIndex>& items, PartialBins& bins_data) {
151153
for (ElementIndex item : items) {
152-
Cost item_weight = model.weights()[item];
154+
Cost item_weight = weights[item];
153155
BaseInt selected_bin = bins_data.bins.size();
154156
for (BaseInt bin = 0; bin < bins_data.bins.size(); ++bin) {
155-
Cost max_load = model.bin_capacity() - item_weight;
157+
Cost max_load = bin_capacity - item_weight;
156158
if (bins_data.loads[bin] <= max_load &&
157159
(selected_bin == bins_data.bins.size() ||
158160
bins_data.loads[bin] > bins_data.loads[selected_bin])) {
@@ -232,9 +234,9 @@ void AddRandomizedBins(const BinPackingModel& model, BaseInt num_bins,
232234
// Generate bins all containing a specific item
233235
for (ElementIndex n : model.ItemRange()) {
234236
BaseInt unique_bin_num = scp_model.full_model().num_subsets();
235-
VLOG_EVERY_N_SEC(1, 5)
236-
<< "Generating bins: " << unique_bin_num << " / " << num_bins << " ("
237-
<< 100.0 * unique_bin_num / num_bins << "%)";
237+
VLOG_EVERY_N_SEC(1, 1)
238+
<< "[RGEN] Generating bins: " << unique_bin_num << " / " << num_bins
239+
<< " (" << 100.0 * unique_bin_num / num_bins << "%)";
238240
if (scp_model.full_model().num_subsets() >= num_bins) {
239241
break;
240242
}
@@ -251,7 +253,7 @@ void AddRandomizedBins(const BinPackingModel& model, BaseInt num_bins,
251253
bins_data.bins.push_back({n});
252254
bins_data.loads.push_back(model.weights()[n]);
253255
}
254-
BestFit(model, items, bins_data);
256+
BestFit(model.weights(), model.bin_capacity(), items, bins_data);
255257
InsertBinsIntoModel(bins_data, scp_model);
256258

257259
items.push_back(n);
@@ -272,51 +274,86 @@ BinPackingSetCoverModel GenerateInitialBins(const BinPackingModel& model) {
272274
std::vector<ElementIndex> items(model.num_items());
273275

274276
absl::c_iota(items, ElementIndex(0));
275-
BestFit(model, items, bins_data);
277+
absl::c_sort(items, [&](auto i1, auto i2) {
278+
return model.weights()[i1] > model.weights()[i2];
279+
});
280+
BestFit(model.weights(), model.bin_capacity(), items, bins_data);
276281
InsertBinsIntoModel(bins_data, scp_model);
277-
BaseInt solution_bin_num = bins_data.bins.size();
278-
VLOG(1) << "Best-fit solution: " << solution_bin_num << " bins";
279-
280-
// Largest first
281-
if (!absl::c_is_sorted(model.weights(), std::greater<>())) {
282-
bins_data.bins.clear();
283-
bins_data.loads.clear();
284-
absl::c_sort(items, [&](auto i1, auto i2) {
285-
return model.weights()[i1] > model.weights()[i2];
286-
});
287-
BestFit(model, items, bins_data);
288-
InsertBinsIntoModel(bins_data, scp_model);
289-
}
282+
VLOG(1) << "[BFIT] Largest first best-fit solution: " << bins_data.bins.size()
283+
<< " bins";
290284

291285
scp_model.CompleteModel();
292286
return scp_model;
293287
}
294288

295-
void ExpKnap::Solve(const ElementCostVector& profits,
296-
const ElementCostVector& weights, Cost capacity,
297-
BaseInt bnb_nodes_limit) {
289+
void ExpKnap::SaveBin() {
290+
collected_bins_.back().clear();
291+
for (ElementIndex i : exceptions_) {
292+
break_selection_[i] = !break_selection_[i];
293+
}
294+
for (ElementIndex i : break_solution_) {
295+
if (break_selection_[i]) {
296+
collected_bins_.back().push_back(i);
297+
}
298+
}
299+
for (ElementIndex i : exceptions_) {
300+
if (break_selection_[i]) {
301+
collected_bins_.back().push_back(i);
302+
}
303+
break_selection_[i] = !break_selection_[i];
304+
}
305+
absl::c_sort(collected_bins_.back());
306+
DCHECK(absl::c_adjacent_find(collected_bins_.back()) ==
307+
collected_bins_.back().end());
308+
}
309+
void ExpKnap::InitSolver(const ElementCostVector& profits,
310+
const ElementCostVector& weights, Cost capacity,
311+
BaseInt bnb_nodes_limit) {
298312
capacity_ = capacity;
299-
best_delta_ = .0;
300-
exceptions_.clear();
301-
maximal_exceptions_.clear();
302-
break_solution_.assign(profits.size(), false);
303313
items_.resize(profits.size());
304-
bnb_node_countdown_ = bnb_nodes_limit;
305-
314+
collected_bins_.clear();
306315
for (ElementIndex i; i < ElementIndex(items_.size()); ++i) {
307-
items_[i] = {std::max(1e-6, profits[i]), weights[i], i};
316+
items_[i] = {profits[i], weights[i], i};
308317
}
309-
310318
absl::c_sort(items_, [](Item i1, Item i2) {
311-
return i1.profit / i1.weight > i2.profit / i2.weight;
319+
return i1.profit / i1.weight < i2.profit / i2.weight;
312320
});
313-
Heuristic(items_);
314-
maximal_exceptions_.push_back(exceptions_);
321+
322+
bnb_node_countdown_ = bnb_nodes_limit;
323+
best_delta_ = .0;
315324
exceptions_.clear();
316-
VLOG(5) << "[KPCG] Heuristic solution: cost "
317-
<< break_profit_sum_ + best_delta_;
325+
break_selection_.assign(profits.size(), false);
326+
break_solution_.clear();
327+
}
328+
329+
void ExpKnap::FindGoodColumns(const ElementCostVector& profits,
330+
const ElementCostVector& weights, Cost capacity,
331+
BaseInt bnb_nodes_limit) {
332+
InitSolver(profits, weights, capacity, bnb_nodes_limit);
333+
Cost curr_best_cost = .0;
334+
PartialBins more_bins;
335+
std::vector<ElementIndex> remaining_items;
318336

319-
EleBranch(.0, break_weight_sum_ - capacity, break_it_ - 1, break_it_ + 1);
337+
bnb_node_countdown_ = bnb_nodes_limit;
338+
inserted_items_.assign(profits.size(), false);
339+
do {
340+
collected_bins_.emplace_back();
341+
Heuristic();
342+
VLOG(5) << "[KPCG] Heuristic solution: cost "
343+
<< break_profit_sum_ + best_delta_;
344+
EleBranch();
345+
346+
for (ElementIndex i : collected_bins_.back()) {
347+
inserted_items_[i] = true;
348+
}
349+
gtl::STLEraseAllFromSequenceIf(
350+
&items_, [&](Item item) { return inserted_items_[item.index]; });
351+
} while (!items_.empty() && break_profit_sum_ + best_delta_ > 1.0);
352+
}
353+
354+
bool ExpKnap::EleBranch() {
355+
exceptions_.clear();
356+
return EleBranch(.0, break_weight_sum_ - capacity_, break_it_ - 1, break_it_);
320357
}
321358

322359
namespace {
@@ -331,6 +368,8 @@ static Cost BoundCheck(Cost best_delta, Cost profit_delta, Cost overweight,
331368
// https://hjemmesider.diku.dk/~pisinger/expknap.c
332369
bool ExpKnap::EleBranch(Cost profit_delta, Cost overweigth, ItemIt out_item,
333370
ItemIt in_item) {
371+
VLOG(6) << "[KPCG] EleBranch: profit_delta " << profit_delta << " overweigth "
372+
<< overweigth;
334373
if (bnb_node_countdown_-- <= 0) {
335374
return false;
336375
}
@@ -345,7 +384,7 @@ bool ExpKnap::EleBranch(Cost profit_delta, Cost overweigth, ItemIt out_item,
345384
}
346385

347386
bool maximal = true;
348-
while (bnb_node_countdown_ > 0 && in_item < items_.end() &&
387+
while (bnb_node_countdown_ > 0 && in_item < items_.rend() &&
349388
BoundCheck(best_delta_, profit_delta, overweigth, *in_item) >= 0) {
350389
exceptions_.push_back(in_item->index);
351390
Cost next_delta = profit_delta + in_item->profit;
@@ -355,11 +394,11 @@ bool ExpKnap::EleBranch(Cost profit_delta, Cost overweigth, ItemIt out_item,
355394
}
356395

357396
if (improved && maximal) {
358-
maximal_exceptions_.push_back(exceptions_);
397+
SaveBin();
359398
}
360399
improved |= !maximal;
361400
} else {
362-
while (bnb_node_countdown_ > 0 && out_item >= items_.begin() &&
401+
while (bnb_node_countdown_ > 0 && out_item >= items_.rbegin() &&
363402
BoundCheck(best_delta_, profit_delta, overweigth, *out_item) >= 0) {
364403
exceptions_.push_back(out_item->index);
365404
Cost next_delta = profit_delta - out_item->profit;
@@ -371,17 +410,18 @@ bool ExpKnap::EleBranch(Cost profit_delta, Cost overweigth, ItemIt out_item,
371410
return improved;
372411
}
373412

374-
void ExpKnap::Heuristic(
375-
const util_intops::StrongVector<ElementIndex, Item>& items) {
413+
void ExpKnap::Heuristic() {
414+
best_delta_ = break_profit_sum_ = break_weight_sum_ = .0;
415+
break_it_ = items_.rbegin();
416+
break_selection_.assign(items_.size(), false);
417+
break_solution_.clear();
376418
exceptions_.clear();
377-
378-
break_profit_sum_ = break_weight_sum_ = .0;
379-
break_it_ = items.begin();
380-
while (break_it_ < items.end() &&
419+
while (break_it_ < items_.rend() &&
381420
break_it_->weight <= capacity_ - break_weight_sum_) {
382421
break_profit_sum_ += break_it_->profit;
383422
break_weight_sum_ += break_it_->weight;
384-
break_solution_[break_it_->index] = true;
423+
break_selection_[break_it_->index] = true;
424+
break_solution_.push_back(break_it_->index);
385425
++break_it_;
386426
}
387427
Cost residual = capacity_ - break_weight_sum_;
@@ -391,74 +431,71 @@ void ExpKnap::Heuristic(
391431

392432
Cost profit_delta_ub = residual * break_it_->profit / break_it_->weight;
393433
if (profit_delta_ub == .0) {
434+
SaveBin();
394435
return;
395436
}
396437

397438
// Try filling the residual space with less efficient (maybe smaller) items
398439
best_delta_ = .0;
399-
for (auto it = break_it_; it < items.end(); it++) {
440+
for (auto it = break_it_; it < items_.rend(); it++) {
400441
if (it->weight <= residual && it->profit > best_delta_) {
401442
exceptions_ = {it->index};
402443
best_delta_ = it->profit;
403444
if (best_delta_ >= profit_delta_ub) {
445+
SaveBin();
404446
return;
405447
}
406448
}
407449
}
408450

409451
// Try removing an item and adding the break item
410452
Cost min_weight = break_it_->weight - residual;
411-
for (auto it = break_it_ - 1; it >= items.begin(); it--) {
453+
for (auto it = break_it_ - 1; it >= items_.rbegin(); it--) {
412454
Cost profit_delta = break_it_->profit - it->profit;
413455
if (it->weight >= min_weight && profit_delta > best_delta_) {
414456
exceptions_ = {break_it_->index, it->index};
415457
best_delta_ = profit_delta;
416458
if (best_delta_ >= profit_delta_ub) {
459+
SaveBin();
417460
return;
418461
}
419462
}
420463
}
464+
SaveBin();
421465
}
422466

423467
bool BinPackingSetCoverModel::UpdateCore(
424468
Cost best_lower_bound, const ElementCostVector& best_multipliers,
425469
const scp::Solution& best_solution, bool force) {
426-
if (--column_gen_countdown_ <= 0 && best_lower_bound != prev_lower_bound_) {
427-
column_gen_countdown_ = column_gen_period_;
470+
if (!base::IsTimeToUpdate(best_lower_bound, force)) {
471+
return false;
472+
}
473+
474+
Cost full_lower_bound = base::UpdateMultipliers(best_multipliers);
475+
if (scp::DivideIfGE0(std::abs(full_lower_bound - best_lower_bound),
476+
best_lower_bound) < 0.01 &&
477+
best_lower_bound != prev_lower_bound_) {
428478
prev_lower_bound_ = best_lower_bound;
429-
knapsack_solver_.Solve(best_multipliers, bpp_model_->weights(),
430-
bpp_model_->bin_capacity(),
431-
/*bnb_nodes_limit=*/10000);
432-
const auto& exception_list = knapsack_solver_.maximal_exceptions();
433-
ElementBoolVector solution = knapsack_solver_.break_solution();
479+
knapsack_solver_.FindGoodColumns(best_multipliers, bpp_model_->weights(),
480+
bpp_model_->bin_capacity(),
481+
/*bnb_nodes_limit=*/1000);
434482
BaseInt num_added_bins = 0;
435-
SparseColumn bin;
436-
for (const std::vector<ElementIndex>& exception : exception_list) {
437-
for (ElementIndex i : exception) {
438-
solution[i] = !solution[i];
439-
}
440-
441-
bin.clear();
442-
for (ElementIndex i : globals_.full_model.ElementRange()) {
443-
if (solution[i]) {
444-
bin.push_back(i);
445-
}
446-
}
483+
for (const SparseColumn& bin : knapsack_solver_.collected_bins()) {
447484
num_added_bins += AddBin(bin) ? 1 : 0;
448-
449-
for (ElementIndex i : exception) {
450-
solution[i] = !solution[i];
451-
}
452485
}
453486
if (num_added_bins > 0) {
454487
VLOG(4) << "[KPCG] Added " << num_added_bins << " / "
455488
<< globals_.full_model.num_subsets() << " bins";
456489
}
490+
base::SizeUpdate();
491+
// TODO(user): add incremental update only for the new columns just added
492+
base::UpdateMultipliers(best_multipliers);
457493
}
458494

459-
scp::FullToCoreModel::SizeUpdate();
460-
scp::FullToCoreModel::FullToSubModelInvariantCheck();
461-
return scp::FullToCoreModel::UpdateCore(best_lower_bound, best_multipliers,
462-
best_solution, force);
495+
if (base::num_focus_subsets() < FixingFullModelView().num_focus_subsets()) {
496+
ComputeAndSetFocus(best_lower_bound, best_solution);
497+
return true;
498+
}
499+
return false;
463500
}
464501
} // namespace operations_research

ortools/algorithms/bin_packing.h

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <absl/strings/str_split.h>
2222
#include <absl/strings/string_view.h>
2323

24+
#include <random>
2425
#include <vector>
2526

2627
#include "ortools/set_cover/base_types.h"
@@ -61,21 +62,26 @@ class ExpKnap {
6162
Cost weight; // weight
6263
ElementIndex index;
6364
};
64-
using ItemIt = std::vector<Item>::const_iterator;
65+
using ItemIt = std::vector<Item>::const_reverse_iterator;
6566

66-
void Solve(const ElementCostVector& profits, const ElementCostVector& weights,
67-
Cost capacity, BaseInt bnb_nodes_limit);
67+
void SaveBin();
68+
void FindGoodColumns(const ElementCostVector& profits,
69+
const ElementCostVector& weights, Cost capacity,
70+
BaseInt bnb_nodes_limit);
71+
void InitSolver(const ElementCostVector& profits,
72+
const ElementCostVector& weights, Cost capacity,
73+
BaseInt bnb_nodes_limit);
6874

75+
bool EleBranch();
6976
bool EleBranch(Cost profit_sum, Cost overweight, ItemIt out_item,
7077
ItemIt in_item);
7178

72-
void Heuristic(const util_intops::StrongVector<ElementIndex, Item>& items);
79+
void Heuristic();
7380

74-
ElementBoolVector break_solution() const { return break_solution_; }
75-
76-
std::vector<std::vector<ElementIndex>> maximal_exceptions() const {
77-
return maximal_exceptions_;
81+
const std::vector<SparseColumn>& collected_bins() const {
82+
return collected_bins_;
7883
}
84+
Cost best_cost() const { return break_profit_sum_ + best_delta_; }
7985

8086
private:
8187
Cost capacity_; // capacity
@@ -85,12 +91,17 @@ class ExpKnap {
8591
Cost break_weight_sum_;
8692
Cost best_delta_;
8793
std::vector<ElementIndex> exceptions_;
88-
std::vector<std::vector<ElementIndex>> maximal_exceptions_;
89-
ElementBoolVector break_solution_;
94+
std::vector<SparseColumn> collected_bins_;
95+
ElementBoolVector break_selection_;
96+
ElementBoolVector inserted_items_;
97+
SparseColumn break_solution_;
9098
BaseInt bnb_node_countdown_;
99+
std::mt19937 rnd_;
91100
};
92101

93102
class BinPackingSetCoverModel : public scp::FullToCoreModel {
103+
using base = scp::FullToCoreModel;
104+
94105
struct BinPackingModelGlobals {
95106
// Dirty hack to avoid invalidation of pointers/references
96107
// A pointer to this data structure is used to compute the hash of bins

0 commit comments

Comments
 (0)