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