OR-Tools  9.6
knapsack_solver_for_cuts.cc
Go to the documentation of this file.
1 // Copyright 2010-2022 Google LLC
2 // Licensed under the Apache License, Version 2.0 (the "License");
3 // you may not use this file except in compliance with the License.
4 // You may obtain a copy of the License at
5 //
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13 
15 
16 #include <algorithm>
17 #include <cstdint>
18 #include <limits>
19 #include <memory>
20 #include <queue>
21 #include <string>
22 #include <utility>
23 #include <vector>
24 
25 #include "ortools/base/logging.h"
26 
27 namespace operations_research {
28 namespace {
29 
30 const int kNoSelection(-1);
31 const double kInfinity = std::numeric_limits<double>::infinity();
32 
33 // Comparator used to sort item in decreasing efficiency order
34 // (see KnapsackCapacityPropagator).
35 struct CompareKnapsackItemsInDecreasingEfficiencyOrder {
36  explicit CompareKnapsackItemsInDecreasingEfficiencyOrder(double _profit_max)
37  : profit_max(_profit_max) {}
38  bool operator()(const KnapsackItemForCutsPtr& item1,
39  const KnapsackItemForCutsPtr& item2) const {
40  return item1->GetEfficiency(profit_max) > item2->GetEfficiency(profit_max);
41  }
42  const double profit_max;
43 };
44 
45 // Comparator used to sort search nodes in the priority queue in order
46 // to pop first the node with the highest profit upper bound
47 // (see KnapsackSearchNodeForCuts). When two nodes have the same upper bound, we
48 // prefer the one with the highest current profit. This is usually the one
49 // closer to a leaf. In practice, the main advantage is to have smaller path.
50 struct CompareKnapsackSearchNodePtrInDecreasingUpperBoundOrder {
51  bool operator()(const KnapsackSearchNodeForCuts* node_1,
52  const KnapsackSearchNodeForCuts* node_2) const {
53  const double profit_upper_bound_1 = node_1->profit_upper_bound();
54  const double profit_upper_bound_2 = node_2->profit_upper_bound();
55  if (profit_upper_bound_1 == profit_upper_bound_2) {
56  return node_1->current_profit() < node_2->current_profit();
57  }
58  return profit_upper_bound_1 < profit_upper_bound_2;
59  }
60 };
61 
62 using SearchQueue = std::priority_queue<
63  KnapsackSearchNodeForCuts*, std::vector<KnapsackSearchNodeForCuts*>,
64  CompareKnapsackSearchNodePtrInDecreasingUpperBoundOrder>;
65 
66 } // namespace
67 
68 // ----- KnapsackSearchNodeForCuts -----
70  const KnapsackSearchNodeForCuts* const parent,
71  const KnapsackAssignmentForCuts& assignment)
72  : depth_(parent == nullptr ? 0 : parent->depth() + 1),
73  parent_(parent),
74  assignment_(assignment),
75  current_profit_(0),
76  profit_upper_bound_(kInfinity),
77  next_item_id_(kNoSelection) {}
78 
79 // ----- KnapsackSearchPathForCuts -----
82  : from_(from), via_(nullptr), to_(to) {}
83 
85  const KnapsackSearchNodeForCuts* node_from =
86  MoveUpToDepth(from_, to_->depth());
87  const KnapsackSearchNodeForCuts* node_to = MoveUpToDepth(to_, from_->depth());
88  DCHECK_EQ(node_from->depth(), node_to->depth());
89 
90  // Find common parent.
91  while (node_from != node_to) {
92  node_from = node_from->parent();
93  node_to = node_to->parent();
94  }
95  via_ = node_from;
96 }
97 
99  const KnapsackSearchNodeForCuts* node, int depth) {
100  while (node->depth() > depth) {
101  node = node->parent();
102  }
103  return node;
104 }
105 
106 // ----- KnapsackStateForCuts -----
107 KnapsackStateForCuts::KnapsackStateForCuts() : is_bound_(), is_in_() {}
108 
109 void KnapsackStateForCuts::Init(int number_of_items) {
110  is_bound_.assign(number_of_items, false);
111  is_in_.assign(number_of_items, false);
112 }
113 
114 // Returns false when the state is invalid.
116  bool revert, const KnapsackAssignmentForCuts& assignment) {
117  if (revert) {
118  is_bound_[assignment.item_id] = false;
119  } else {
120  if (is_bound_[assignment.item_id] &&
121  is_in_[assignment.item_id] != assignment.is_in) {
122  return false;
123  }
124  is_bound_[assignment.item_id] = true;
125  is_in_[assignment.item_id] = assignment.is_in;
126  }
127  return true;
128 }
129 
130 // ----- KnapsackPropagatorForCuts -----
132  const KnapsackStateForCuts* state)
133  : items_(),
134  current_profit_(0),
135  profit_lower_bound_(0),
136  profit_upper_bound_(kInfinity),
137  state_(state) {}
138 
140 
141 void KnapsackPropagatorForCuts::Init(const std::vector<double>& profits,
142  const std::vector<double>& weights,
143  const double capacity) {
144  const int number_of_items = profits.size();
145  items_.clear();
146 
147  for (int i = 0; i < number_of_items; ++i) {
148  items_.emplace_back(
149  std::make_unique<KnapsackItemForCuts>(i, weights[i], profits[i]));
150  }
151  capacity_ = capacity;
152  current_profit_ = 0;
153  profit_lower_bound_ = -kInfinity;
154  profit_upper_bound_ = kInfinity;
155  InitPropagator();
156 }
157 
159  bool revert, const KnapsackAssignmentForCuts& assignment) {
160  if (assignment.is_in) {
161  if (revert) {
162  current_profit_ -= items_[assignment.item_id]->profit;
163  consumed_capacity_ -= items()[assignment.item_id]->weight;
164  } else {
165  current_profit_ += items_[assignment.item_id]->profit;
166  consumed_capacity_ += items()[assignment.item_id]->weight;
167  if (consumed_capacity_ > capacity_) {
168  return false;
169  }
170  }
171  }
172  return true;
173 }
174 
176  std::vector<bool>* solution) const {
177  DCHECK(solution != nullptr);
178  for (int i(0); i < items_.size(); ++i) {
179  const int item_id = items_[i]->id;
180  (*solution)[item_id] = state_->is_bound(item_id) && state_->is_in(item_id);
181  }
182  double remaining_capacity = capacity_ - consumed_capacity_;
183  for (const KnapsackItemForCutsPtr& item : sorted_items_) {
184  if (!state().is_bound(item->id)) {
185  if (remaining_capacity >= item->weight) {
186  remaining_capacity -= item->weight;
187  (*solution)[item->id] = true;
188  } else {
189  return;
190  }
191  }
192  }
193 }
194 
197  break_item_id_ = kNoSelection;
198 
199  double remaining_capacity = capacity_ - consumed_capacity_;
200  int break_sorted_item_id = kNoSelection;
201  for (int sorted_id(0); sorted_id < sorted_items_.size(); ++sorted_id) {
202  if (!state().is_bound(sorted_items_[sorted_id]->id)) {
203  const KnapsackItemForCutsPtr& item = sorted_items_[sorted_id];
204  break_item_id_ = item->id;
205  if (remaining_capacity >= item->weight) {
206  remaining_capacity -= item->weight;
207  set_profit_lower_bound(profit_lower_bound() + item->profit);
208  } else {
209  break_sorted_item_id = sorted_id;
210  break;
211  }
212  }
213  }
214 
216  // If break_sorted_item_id == kNoSelection, then all remaining items fit into
217  // the knapsack, and thus the lower bound on the profit equals the upper
218  // bound. Otherwise, we compute a tight upper bound by filling the remaining
219  // capacity of the knapsack with "fractional" items, in the decreasing order
220  // of their efficiency.
221  if (break_sorted_item_id != kNoSelection) {
222  const double additional_profit =
223  GetAdditionalProfitUpperBound(remaining_capacity, break_sorted_item_id);
224  set_profit_upper_bound(profit_upper_bound() + additional_profit);
225  }
226 }
227 
229  consumed_capacity_ = 0;
230  break_item_id_ = kNoSelection;
231  sorted_items_.clear();
232  sorted_items_.reserve(items().size());
233  for (int i(0); i < items().size(); ++i) {
234  sorted_items_.emplace_back(std::make_unique<KnapsackItemForCuts>(
235  i, items()[i]->weight, items()[i]->profit));
236  }
237  profit_max_ = 0;
238  for (const KnapsackItemForCutsPtr& item : sorted_items_) {
239  profit_max_ = std::max(profit_max_, item->profit);
240  }
241  profit_max_ += 1.0;
242  CompareKnapsackItemsInDecreasingEfficiencyOrder compare_object(profit_max_);
243  std::sort(sorted_items_.begin(), sorted_items_.end(), compare_object);
244 }
245 
246 double KnapsackPropagatorForCuts::GetAdditionalProfitUpperBound(
247  double remaining_capacity, int break_item_id) const {
248  const int after_break_item_id = break_item_id + 1;
249  double additional_profit_when_no_break_item = 0;
250  if (after_break_item_id < sorted_items_.size()) {
251  // As items are sorted by decreasing profit / weight ratio, and the current
252  // weight is non-zero, the next_weight is non-zero too.
253  const double next_weight = sorted_items_[after_break_item_id]->weight;
254  const double next_profit = sorted_items_[after_break_item_id]->profit;
255  additional_profit_when_no_break_item =
256  std::max((remaining_capacity * next_profit) / next_weight, 0.0);
257  }
258 
259  const int before_break_item_id = break_item_id - 1;
260  double additional_profit_when_break_item = 0;
261  if (before_break_item_id >= 0) {
262  const double previous_weight = sorted_items_[before_break_item_id]->weight;
263  // Having previous_weight == 0 means the total capacity is smaller than
264  // the weight of the current item. In such a case the item cannot be part
265  // of a solution of the local one dimension problem.
266  if (previous_weight != 0) {
267  const double previous_profit =
268  sorted_items_[before_break_item_id]->profit;
269  const double overused_capacity =
270  sorted_items_[break_item_id]->weight - remaining_capacity;
271  const double lost_profit_from_previous_item =
272  (overused_capacity * previous_profit) / previous_weight;
273  additional_profit_when_break_item = std::max(
274  sorted_items_[break_item_id]->profit - lost_profit_from_previous_item,
275  0.0);
276  }
277  }
278 
279  const double additional_profit = std::max(
280  additional_profit_when_no_break_item, additional_profit_when_break_item);
281  return additional_profit;
282 }
283 
284 // ----- KnapsackSolverForCuts -----
286  : propagator_(&state_),
287  best_solution_profit_(0),
288  solver_name_(std::move(solver_name)) {}
289 
290 void KnapsackSolverForCuts::Init(const std::vector<double>& profits,
291  const std::vector<double>& weights,
292  const double capacity) {
293  const int number_of_items(profits.size());
294  state_.Init(number_of_items);
295  best_solution_.assign(number_of_items, false);
296  CHECK_EQ(number_of_items, weights.size());
297 
298  propagator_.Init(profits, weights, capacity);
299 }
300 
302  bool is_item_in,
303  double* lower_bound,
304  double* upper_bound) {
305  DCHECK(lower_bound != nullptr);
306  DCHECK(upper_bound != nullptr);
307  KnapsackAssignmentForCuts assignment(item_id, is_item_in);
308  const bool fail = !IncrementalUpdate(false, assignment);
309  if (fail) {
310  *lower_bound = 0;
311  *upper_bound = 0;
312  } else {
313  *lower_bound = propagator_.profit_lower_bound();
314  *upper_bound = GetAggregatedProfitUpperBound();
315  }
316 
317  const bool fail_revert = !IncrementalUpdate(true, assignment);
318  if (fail_revert) {
319  *lower_bound = 0;
320  *upper_bound = 0;
321  }
322 }
323 
325  bool* is_solution_optimal) {
326  DCHECK(time_limit != nullptr);
327  DCHECK(is_solution_optimal != nullptr);
328  best_solution_profit_ = 0;
329  *is_solution_optimal = true;
330 
331  SearchQueue search_queue;
332  const KnapsackAssignmentForCuts assignment(kNoSelection, true);
333  auto root_node =
334  std::make_unique<KnapsackSearchNodeForCuts>(nullptr, assignment);
335  root_node->set_current_profit(GetCurrentProfit());
336  root_node->set_profit_upper_bound(GetAggregatedProfitUpperBound());
337  root_node->set_next_item_id(GetNextItemId());
338  search_nodes_.push_back(std::move(root_node));
339  const KnapsackSearchNodeForCuts* current_node =
340  search_nodes_.back().get(); // Start with the root node.
341 
342  if (MakeNewNode(*current_node, false)) {
343  search_queue.push(search_nodes_.back().get());
344  }
345  if (MakeNewNode(*current_node, true)) {
346  search_queue.push(search_nodes_.back().get());
347  }
348 
349  int64_t number_of_nodes_visited = 0;
350  while (!search_queue.empty() &&
351  search_queue.top()->profit_upper_bound() > best_solution_profit_) {
352  if (time_limit->LimitReached()) {
353  *is_solution_optimal = false;
354  break;
355  }
356  if (solution_upper_bound_threshold_ > -kInfinity &&
357  GetAggregatedProfitUpperBound() < solution_upper_bound_threshold_) {
358  *is_solution_optimal = false;
359  break;
360  }
361  if (best_solution_profit_ > solution_lower_bound_threshold_) {
362  *is_solution_optimal = false;
363  break;
364  }
365  if (number_of_nodes_visited >= node_limit_) {
366  *is_solution_optimal = false;
367  break;
368  }
369  KnapsackSearchNodeForCuts* const node = search_queue.top();
370  search_queue.pop();
371 
372  if (node != current_node) {
373  KnapsackSearchPathForCuts path(current_node, node);
374  path.Init();
375  CHECK_EQ(UpdatePropagators(path), true);
376  current_node = node;
377  }
378  number_of_nodes_visited++;
379 
380  if (MakeNewNode(*node, false)) {
381  search_queue.push(search_nodes_.back().get());
382  }
383  if (MakeNewNode(*node, true)) {
384  search_queue.push(search_nodes_.back().get());
385  }
386  }
387  return best_solution_profit_;
388 }
389 
390 // Returns false when at least one propagator fails.
391 bool KnapsackSolverForCuts::UpdatePropagators(
392  const KnapsackSearchPathForCuts& path) {
393  bool no_fail = true;
394  // Revert previous changes.
395  const KnapsackSearchNodeForCuts* node = &path.from();
396  const KnapsackSearchNodeForCuts* const via = &path.via();
397  while (node != via) {
398  no_fail = IncrementalUpdate(true, node->assignment()) && no_fail;
399  node = node->parent();
400  }
401  // Apply current changes.
402  node = &path.to();
403  while (node != via) {
404  no_fail = IncrementalUpdate(false, node->assignment()) && no_fail;
405  node = node->parent();
406  }
407  return no_fail;
408 }
409 
410 double KnapsackSolverForCuts::GetAggregatedProfitUpperBound() {
411  propagator_.ComputeProfitBounds();
412  const double propagator_upper_bound = propagator_.profit_upper_bound();
413  return std::min(kInfinity, propagator_upper_bound);
414 }
415 
416 bool KnapsackSolverForCuts::MakeNewNode(const KnapsackSearchNodeForCuts& node,
417  bool is_in) {
418  if (node.next_item_id() == kNoSelection) {
419  return false;
420  }
421  KnapsackAssignmentForCuts assignment(node.next_item_id(), is_in);
422  KnapsackSearchNodeForCuts new_node(&node, assignment);
423 
424  KnapsackSearchPathForCuts path(&node, &new_node);
425  path.Init();
426  const bool no_fail = UpdatePropagators(path);
427  if (no_fail) {
428  new_node.set_current_profit(GetCurrentProfit());
429  new_node.set_profit_upper_bound(GetAggregatedProfitUpperBound());
430  new_node.set_next_item_id(GetNextItemId());
431  UpdateBestSolution();
432  }
433 
434  // Revert to be able to create another node from parent.
435  KnapsackSearchPathForCuts revert_path(&new_node, &node);
436  revert_path.Init();
437  UpdatePropagators(revert_path);
438 
439  if (!no_fail || new_node.profit_upper_bound() < best_solution_profit_) {
440  return false;
441  }
442 
443  // The node is relevant.
444  auto relevant_node =
445  std::make_unique<KnapsackSearchNodeForCuts>(&node, assignment);
446  relevant_node->set_current_profit(new_node.current_profit());
447  relevant_node->set_profit_upper_bound(new_node.profit_upper_bound());
448  relevant_node->set_next_item_id(new_node.next_item_id());
449  search_nodes_.push_back(std::move(relevant_node));
450 
451  return true;
452 }
453 
454 bool KnapsackSolverForCuts::IncrementalUpdate(
455  bool revert, const KnapsackAssignmentForCuts& assignment) {
456  // Do not stop on a failure: To be able to be incremental on the update,
457  // partial solution (state) and propagators must all be in the same state.
458  bool no_fail = state_.UpdateState(revert, assignment);
459  no_fail = propagator_.Update(revert, assignment) && no_fail;
460  return no_fail;
461 }
462 
463 void KnapsackSolverForCuts::UpdateBestSolution() {
464  const double profit_lower_bound = propagator_.profit_lower_bound();
465 
466  if (best_solution_profit_ < profit_lower_bound) {
467  best_solution_profit_ = profit_lower_bound;
468  propagator_.CopyCurrentStateToSolution(&best_solution_);
469  }
470 }
471 
472 } // namespace operations_research
int64_t max
Definition: alldiff_cst.cc:140
int64_t min
Definition: alldiff_cst.cc:139
void Init(const std::vector< double > &profits, const std::vector< double > &weights, double capacity)
void CopyCurrentStateToSolution(std::vector< bool > *solution) const
const std::vector< KnapsackItemForCutsPtr > & items() const
KnapsackPropagatorForCuts(const KnapsackStateForCuts *state)
bool Update(bool revert, const KnapsackAssignmentForCuts &assignment)
const KnapsackSearchNodeForCuts *const parent() const
const KnapsackAssignmentForCuts & assignment() const
KnapsackSearchNodeForCuts(const KnapsackSearchNodeForCuts *parent, const KnapsackAssignmentForCuts &assignment)
const KnapsackSearchNodeForCuts & from() const
const KnapsackSearchNodeForCuts & via() const
const KnapsackSearchNodeForCuts & to() const
KnapsackSearchPathForCuts(const KnapsackSearchNodeForCuts *from, const KnapsackSearchNodeForCuts *to)
void Init(const std::vector< double > &profits, const std::vector< double > &weights, const double capacity)
double Solve(TimeLimit *time_limit, bool *is_solution_optimal)
void GetLowerAndUpperBoundWhenItem(int item_id, bool is_item_in, double *lower_bound, double *upper_bound)
bool UpdateState(bool revert, const KnapsackAssignmentForCuts &assignment)
A simple class to enforce both an elapsed time limit and a deterministic time limit in the same threa...
Definition: time_limit.h:106
ModelSharedTimeLimit * time_limit
const double profit_max
Collection of objects used to extend the Constraint Solver library.
std::unique_ptr< KnapsackItemForCuts > KnapsackItemForCutsPtr
const KnapsackSearchNodeForCuts * MoveUpToDepth(const KnapsackSearchNodeForCuts *node, int depth)
int64_t weight
Definition: pack.cc:510
IntVar * upper_bound
Definition: routing.cc:1087
IntVar * lower_bound
Definition: routing.cc:1086
int64_t capacity
constexpr double kInfinity