OR-Tools  9.6
pricing.h
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 
14 #ifndef OR_TOOLS_GLOP_PRICING_H_
15 #define OR_TOOLS_GLOP_PRICING_H_
16 
17 #include <random>
18 #include <string>
19 
20 #include "absl/random/bit_gen_ref.h"
21 #include "absl/random/random.h"
23 #include "ortools/util/bitset.h"
24 #include "ortools/util/stats.h"
25 
26 namespace operations_research {
27 namespace glop {
28 
29 // Maintains a set of elements in [0, n), each with an associated value and
30 // allows to query the element of maximum value efficiently.
31 //
32 // This is optimized for use in the pricing step of the simplex algorithm.
33 // Basically at each simplex iterations, you want to:
34 //
35 // 1/ Get the candidate with the maximum value. The number of candidates
36 // can be close to n, or really small. You also want some randomization if
37 // several elements have an equivalent (maximum) value.
38 //
39 // 2/ Update the set of candidate and their values, where the number of update
40 // is usually a lot smaller than n. Note that in some corner cases, there are
41 // two "updates" phases, so a position can be updated twice.
42 //
43 // The idea is to be faster than O(num_candidates) per GetMaximum(), most of the
44 // time. All updates should be in O(1) with as little overhead as possible. The
45 // algorithm here dynamically maintain the top-k (for k=32) with best effort and
46 // use it instead of doing a O(num_candidates) scan when possible.
47 //
48 // Note that when O(num_updates) << n, this can have a huge effect. A basic O(1)
49 // per update, O(num_candidates) per maximum query was taking around 60% of the
50 // total time on graph40-80-1rand.pb.gz ! with the top-32 algo coded here, it is
51 // around 3%, and the number of "fast" GetMaximum() that hit the top-k heap on
52 // the first 120s of that problem was 250757 / 255659. Note that n was 282624 in
53 // this case, which is not even the biggest size we can tackle.
54 //
55 // Note(user): This could be moved to util/ as a general class if someone wants
56 // to reuse it, it is however tunned for use in Glop pricing step and might
57 // becomes even more specific in the future.
58 template <typename Index>
60  public:
61  // To simplify the APIs, we take a random number generator at construction.
62  explicit DynamicMaximum(absl::BitGenRef random) : random_(random) {}
63 
64  // Prepares the class to hold up to n candidates with indices in [0, n).
65  // Initially no indices is a candidate.
67 
68  // Returns the index with the maximum value or Index(-1) if the set is empty
69  // and there is no possible candidate. If there are more than one candidate
70  // with the same maximum value, this will return a random one (not always
71  // uniformly if there is a large number of ties).
73 
74  // Removes the given index from the set of candidates.
75  void Remove(Index position);
76 
77  // Adds an element to the set of candidate and sets its value. If the element
78  // is already present, this updates its value. The value must be finite.
79  void AddOrUpdate(Index position, Fractional value);
80 
81  // Optimized version of AddOrUpdate() for the dense case. If one knows that
82  // there will be O(n) updates, it is possible to call StartDenseUpdates() and
83  // then use DenseAddOrUpdate() instead of AddOrUpdate() which is slighlty
84  // faster.
85  //
86  // Note that calling AddOrUpdate() will still works fine, but will cause an
87  // extra test per call.
90 
91  // Returns the current size n that was used in the last ClearAndResize().
92  void Clear() { ClearAndResize(Index(0)); }
93  Index Size() const { return values_.size(); }
94 
95  // Returns some stats about this class if they are enabled.
96  std::string StatString() const { return stats_.StatString(); }
97 
98  private:
99  // Adds an elements to the set of top elements.
100  void UpdateTopK(Index position, Fractional value);
101 
102  // Returns a random element from the set {best} U {equivalent_choices_}.
103  // If equivalent_choices_ is empty, this just returns best.
104  Index RandomizeIfManyChoices(Index best);
105 
106  // For tie-breaking.
107  absl::BitGenRef random_;
108  std::vector<Index> equivalent_choices_;
109 
110  // Set of candidates and their value.
111  // Note that if is_candidate_[index] is false, values_[index] can be anything.
113  Bitset64<Index> is_candidate_;
114 
115  // We maintain the top-k current candidates for a fixed k. Note that not all
116  // entries in tops_ are necessary up to date since we don't remove elements.
117  // There can even be duplicate elements inside if Update() add an element
118  // already inside. This is fine, since tops_ will be recomputed as soon as we
119  // can't get the true maximum from there.
120  //
121  // The invariant is that:
122  // - All elements > threshold_ are in tops_.
123  // - All elements not in tops have a value <= threshold_.
124  // - elements == threshold_ can be in or out.
125  //
126  // In particular, the threshold only increase until the heap becomes empty and
127  // is recomputed from scratch by GetMaximum().
128  struct HeapElement {
129  HeapElement() {}
130  HeapElement(Index i, Fractional v) : index(i), value(v) {}
131 
132  Index index;
134 
135  // We want a min-heap: tops_.top() actually represents the k-th value, not
136  // the max.
137  const double operator<(const HeapElement& other) const {
138  return value > other.value;
139  }
140  };
141  Fractional threshold_;
142  std::vector<HeapElement> tops_;
143 
144  // Statistics about the class.
145  struct QueryStats : public StatsGroup {
146  QueryStats()
147  : StatsGroup("PricingStats"),
148  get_maximum("get_maximum", this),
149  heap_size_on_hit("heap_size_on_hit", this),
150  random_choices("random_choices", this) {}
151  TimeDistribution get_maximum;
152  IntegerDistribution heap_size_on_hit;
153  IntegerDistribution random_choices;
154  };
155  QueryStats stats_;
156 };
157 
158 template <typename Index>
160  tops_.clear();
161  threshold_ = -kInfinity;
162  values_.resize(n);
163  is_candidate_.ClearAndResize(n);
164 }
165 
166 template <typename Index>
167 inline void DynamicMaximum<Index>::Remove(Index position) {
168  is_candidate_.Clear(position);
169 }
170 
171 template <typename Index>
173  // This disable tops_ until the next GetMaximum().
174  tops_.clear();
175  threshold_ = kInfinity;
176 }
177 
178 template <typename Index>
180  Fractional value) {
181  DCHECK(IsFinite(value));
182  DCHECK(tops_.empty());
183  is_candidate_.Set(position);
184  values_[position] = value;
185 }
186 
187 template <typename Index>
189  Fractional value) {
190  DCHECK(IsFinite(value));
191  is_candidate_.Set(position);
192  values_[position] = value;
193  if (value >= threshold_) UpdateTopK(position, value);
194 }
195 
196 template <typename Index>
198  if (equivalent_choices_.empty()) return best;
199  equivalent_choices_.push_back(best);
200  stats_.random_choices.Add(equivalent_choices_.size());
201 
202  return equivalent_choices_[std::uniform_int_distribution<int>(
203  0, equivalent_choices_.size() - 1)(random_)];
204 }
205 
206 template <typename Index>
208  SCOPED_TIME_STAT(&stats_);
209  Fractional best_value = -kInfinity;
210  Index best_position(-1);
211  equivalent_choices_.clear();
212 
213  // Optimized version if the maximum is in tops_ already.
214  //
215  // We do two things here:
216  // 1/ Filter tops_ to only contain valid entries. This is because we never
217  // remove element, so the value of one of the element in tops might have
218  // decreased now. Note that we leave threshold_ untouched, so it
219  // can actually be lower than the minimum of the element in tops.
220  // 2/ Get the maximum of the valid elements.
221  if (!tops_.empty()) {
222  int new_size = 0;
223  for (const HeapElement e : tops_) {
224  // The two possible sources of "invalidity".
225  if (!is_candidate_[e.index]) continue;
226  if (values_[e.index] != e.value) continue;
227 
228  tops_[new_size++] = e;
229  if (e.value >= best_value) {
230  if (e.value == best_value) {
231  equivalent_choices_.push_back(e.index);
232  continue;
233  }
234  equivalent_choices_.clear();
235  best_value = e.value;
236  best_position = e.index;
237  }
238  }
239  tops_.resize(new_size);
240  if (new_size != 0) {
241  stats_.heap_size_on_hit.Add(new_size);
242  return RandomizeIfManyChoices(best_position);
243  }
244  }
245 
246  // We need to iterate over all the candidates.
247  threshold_ = -kInfinity;
248  DCHECK(tops_.empty());
249  for (const Index position : is_candidate_) {
250  const Fractional value = values_[position];
251 
252  // TODO(user): Add a mode when we do not maintain the TopK for small sizes
253  // (like n < 1000) ? The gain might not be worth the extra code though.
254  if (value < threshold_) continue;
255  UpdateTopK(position, value);
256 
257  if (value >= best_value) {
258  if (value == best_value) {
259  equivalent_choices_.push_back(position);
260  continue;
261  }
262  equivalent_choices_.clear();
263  best_value = value;
264  best_position = position;
265  }
266  }
267 
268  return RandomizeIfManyChoices(best_position);
269 }
270 
271 template <typename Index>
272 inline void DynamicMaximum<Index>::UpdateTopK(Index position,
273  Fractional value) {
274  // Note that this should only be called when an update is required.
275  DCHECK_GE(value, threshold_);
276 
277  // We use a compile time size of the form 2^n - 1 to have a full binary heap.
278  //
279  // TODO(user): Adapt the size depending on the problem size? Note sure it is
280  // worth it. To experiment more.
281  constexpr int k = 31;
282  static_assert(((k + 1) & k) == 0, "k + 1 should be a power of 2.");
283 
284  // Simply grow the vector until we hit a size of k.
285  if (tops_.size() < k) {
286  tops_.emplace_back(position, value);
287  if (tops_.size() == k) {
288  std::make_heap(tops_.begin(), tops_.end());
289  threshold_ = tops_[0].value;
290  }
291  return;
292  }
293 
294  // If the value is equal, we randomly replace it. Having some randomness can
295  // also be important to increase the chance of keeping the true maximum in the
296  // top k set.
297  //
298  // TODO(user): use proper probability by counting the number of ties seen and
299  // replacing a random minimum element to get an uniform distribution? Note
300  // that it will never be truly uniform since once the top k structure is
301  // constructed, we will reuse it as much as possible, so it will be biased
302  // towards elements already inside.
303  if (value == tops_[0].value) {
304  if (absl::Bernoulli(random_, 0.5)) {
305  tops_[0].index = position;
306  }
307  return;
308  }
309 
310  // The code below is basically a custom implementation of this. It is however
311  // only slighlty faster for such a small heap. So it might not be completely
312  // worth it.
313  if (/*DISABLES CODE*/ (false)) {
314  std::pop_heap(tops_.begin(), tops_.end());
315  tops_.back() = HeapElement(position, value);
316  std::push_heap(tops_.begin(), tops_.end());
317  threshold_ = tops_[0].value;
318  return;
319  }
320 
321  // To not have to do std::pop_heap() and then std::push_heap(), we code our
322  // own update. Note that we exploit the fact that k is of the form 2^n - 1 to
323  // save one test per update.
324  int i = 0;
325  DCHECK_EQ(tops_.size(), k);
326  constexpr int limit = k / 2;
327  for (; i < limit;) {
328  const int left_child = 2 * i + 1;
329  const int right_child = left_child + 1;
330  const Fractional l_value = tops_[left_child].value;
331  const Fractional r_value = tops_[right_child].value;
332  if (l_value > r_value) {
333  if (value <= r_value) break;
334  tops_[i] = tops_[right_child];
335  i = right_child;
336  } else {
337  if (value <= l_value) break;
338  tops_[i] = tops_[left_child];
339  i = left_child;
340  }
341  }
342  tops_[i] = HeapElement(position, value);
343  threshold_ = tops_[0].value;
344  DCHECK(std::is_heap(tops_.begin(), tops_.end()));
345 }
346 
347 } // namespace glop
348 } // namespace operations_research
349 
350 #endif // OR_TOOLS_GLOP_PRICING_H_
int right_child
StatsGroup(absl::string_view name)
Definition: stats.h:140
void AddOrUpdate(Index position, Fractional value)
Definition: pricing.h:188
DynamicMaximum(absl::BitGenRef random)
Definition: pricing.h:62
void DenseAddOrUpdate(Index position, Fractional value)
Definition: pricing.h:179
int64_t value
int index
constexpr double kInfinity
Definition: lp_types.h:88
bool IsFinite(Fractional value)
Definition: lp_types.h:95
Collection of objects used to extend the Constraint Solver library.
#define SCOPED_TIME_STAT(stats)
Definition: stats.h:439