ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
FedergruenTzurCandidateTree.cs
Go to the documentation of this file.
1using System.Buffers;
2
4
5/// <summary>
6/// Array-backed balanced candidate tree for the Federgruen-Tzur forward algorithm.
7/// </summary>
8/// <remarks>
9/// <para>
10/// Candidate periods are ordered by their transformed variable-cost coefficient.
11/// The doubly linked order represents the Minimal Optimal Predecessor envelope,
12/// while the AVL links provide logarithmic insertion and deletion without
13/// allocating one managed object per candidate.
14/// </para>
15/// <para>
16/// For two adjacent candidates, <c>StartX</c> is the cumulative-demand threshold
17/// at which the lower-slope candidate becomes at least as attractive as its
18/// predecessor. These thresholds are the geometric counterpart of the
19/// <c>G(k,l)</c> values in Federgruen and Tzur (1991).
20/// </para>
21/// </remarks>
22internal sealed class FedergruenTzurCandidateTree : IDisposable
23{
24 private readonly int _capacity;
25
26 private readonly double[] _slope;
27 private readonly double[] _intercept;
28 private readonly double[] _startX;
29
30 private readonly int[] _left;
31 private readonly int[] _right;
32 private readonly int[] _parent;
33 private readonly int[] _height;
34 private readonly int[] _previous;
35 private readonly int[] _next;
36
37 private int _root = -1;
38 private int _first = -1;
39 private bool _disposed;
40
41 public FedergruenTzurCandidateTree(int capacity)
42 {
43 if (capacity <= 0)
44 {
45 throw new ArgumentOutOfRangeException(
46 nameof(capacity),
47 capacity,
48 "Candidate-tree capacity must be positive.");
49 }
50
51 _capacity = capacity;
52
53 _slope = ArrayPool<double>.Shared.Rent(capacity);
54 _intercept = ArrayPool<double>.Shared.Rent(capacity);
55 _startX = ArrayPool<double>.Shared.Rent(capacity);
56
57 _left = ArrayPool<int>.Shared.Rent(capacity);
58 _right = ArrayPool<int>.Shared.Rent(capacity);
59 _parent = ArrayPool<int>.Shared.Rent(capacity);
60 _height = ArrayPool<int>.Shared.Rent(capacity);
61 _previous = ArrayPool<int>.Shared.Rent(capacity);
62 _next = ArrayPool<int>.Shared.Rent(capacity);
63 }
64
65 public double GetSlope(int period)
66 {
67 ValidateNode(period);
68 return _slope[period];
69 }
70
71 public double GetIntercept(int period)
72 {
73 ValidateNode(period);
74 return _intercept[period];
75 }
76
77 /// <summary>
78 /// Inserts one candidate line and removes candidates that can never be
79 /// optimal for any future cumulative demand.
80 /// </summary>
81 /// <returns>
82 /// <see langword="true"/> if the candidate remains in the minimal envelope;
83 /// otherwise <see langword="false"/>.
84 /// </returns>
85 public bool Add(
86 int period,
87 double slope,
88 double intercept)
89 {
90 ThrowIfDisposed();
91
92 if ((uint)period >= (uint)_capacity)
93 {
94 throw new ArgumentOutOfRangeException(nameof(period));
95 }
96
97 if (!double.IsFinite(slope) || !double.IsFinite(intercept))
98 {
99 throw new ArithmeticException(
100 "Federgruen-Tzur candidate coefficients must be finite.");
101 }
102
103 var equalSlope = FindBySlope(slope);
104
105 if (equalSlope >= 0)
106 {
107 // A lower intercept dominates globally. With equal intercepts the
108 // earlier period is retained, which is Federgruen-Tzur's canonical
109 // lowest-index tie breaking because periods arrive chronologically.
110 if (_intercept[equalSlope] <= intercept)
111 {
112 return false;
113 }
114
115 Remove(equalSlope);
116 }
117
118 InitializeNode(period, slope, intercept);
119
120 var higherSlope = -1;
121 var lowerSlope = -1;
122 var treeParent = -1;
123 var current = _root;
124
125 while (current >= 0)
126 {
127 treeParent = current;
128
129 if (slope < _slope[current])
130 {
131 higherSlope = current;
132 current = _left[current];
133 }
134 else
135 {
136 lowerSlope = current;
137 current = _right[current];
138 }
139 }
140
141 _parent[period] = treeParent;
142
143 if (treeParent < 0)
144 {
145 _root = period;
146 }
147 else if (slope < _slope[treeParent])
148 {
149 _left[treeParent] = period;
150 }
151 else
152 {
153 _right[treeParent] = period;
154 }
155
156 // Hull order is nonincreasing in slope:
157 // Previous = higher slope, Next = lower slope.
158 _previous[period] = higherSlope;
159 _next[period] = lowerSlope;
160
161 if (higherSlope >= 0)
162 {
163 _next[higherSlope] = period;
164 }
165 else
166 {
167 _first = period;
168 }
169
170 if (lowerSlope >= 0)
171 {
172 _previous[lowerSlope] = period;
173 }
174
175 Rebalance(treeParent);
176
177 if (higherSlope >= 0 &&
178 lowerSlope >= 0 &&
179 IntersectionX(higherSlope, period) >=
180 IntersectionX(period, lowerSlope))
181 {
182 Remove(period);
183 return false;
184 }
185
186 var previous = _previous[period];
187
188 while (previous >= 0)
189 {
190 var previousPrevious = _previous[previous];
191
192 if (previousPrevious < 0 ||
193 IntersectionX(previousPrevious, previous) <
194 IntersectionX(previous, period))
195 {
196 break;
197 }
198
199 Remove(previous);
200 previous = _previous[period];
201 }
202
203 var next = _next[period];
204
205 while (next >= 0)
206 {
207 var nextNext = _next[next];
208
209 if (nextNext < 0 ||
210 IntersectionX(period, next) <
211 IntersectionX(next, nextNext))
212 {
213 break;
214 }
215
216 Remove(next);
217 next = _next[period];
218 }
219
220 previous = _previous[period];
221 next = _next[period];
222
223 _startX[period] =
224 previous < 0
225 ? double.NegativeInfinity
226 : IntersectionX(previous, period);
227
228 if (next >= 0)
229 {
230 _startX[next] = IntersectionX(period, next);
231 }
232
233 return true;
234 }
235
236 /// <summary>
237 /// Removes envelope candidates whose optimality interval lies entirely
238 /// before the current cumulative demand and returns the current best period.
239 /// </summary>
240 public int GetBestAndDiscardPast(double cumulativeDemand)
241 {
242 ThrowIfDisposed();
243
244 if (!double.IsFinite(cumulativeDemand))
245 {
246 throw new ArithmeticException(
247 "Cumulative demand must be finite.");
248 }
249
250 if (_first < 0)
251 {
252 throw new InvalidOperationException(
253 "The Federgruen-Tzur candidate tree is empty.");
254 }
255
256 while (_next[_first] >= 0)
257 {
258 var current = _first;
259 var next = _next[current];
260 var threshold = _startX[next];
261
262 var nextIsPreferred =
263 threshold < cumulativeDemand ||
264 (threshold == cumulativeDemand && next < current);
265
266 if (!nextIsPreferred)
267 {
268 break;
269 }
270
271 Remove(current);
272 }
273
274 return _first;
275 }
276
277 public void Dispose()
278 {
279 if (_disposed)
280 {
281 return;
282 }
283
284 _disposed = true;
285
286 ArrayPool<double>.Shared.Return(_slope, clearArray: false);
287 ArrayPool<double>.Shared.Return(_intercept, clearArray: false);
288 ArrayPool<double>.Shared.Return(_startX, clearArray: false);
289
290 ArrayPool<int>.Shared.Return(_left, clearArray: false);
291 ArrayPool<int>.Shared.Return(_right, clearArray: false);
292 ArrayPool<int>.Shared.Return(_parent, clearArray: false);
293 ArrayPool<int>.Shared.Return(_height, clearArray: false);
294 ArrayPool<int>.Shared.Return(_previous, clearArray: false);
295 ArrayPool<int>.Shared.Return(_next, clearArray: false);
296 }
297
298 private void InitializeNode(
299 int period,
300 double slope,
301 double intercept)
302 {
303 _slope[period] = slope;
304 _intercept[period] = intercept;
305 _startX[period] = double.NegativeInfinity;
306
307 _left[period] = -1;
308 _right[period] = -1;
309 _parent[period] = -1;
310 _height[period] = 1;
311 _previous[period] = -1;
312 _next[period] = -1;
313 }
314
315 private int FindBySlope(double slope)
316 {
317 var current = _root;
318
319 while (current >= 0)
320 {
321 if (slope < _slope[current])
322 {
323 current = _left[current];
324 }
325 else if (slope > _slope[current])
326 {
327 current = _right[current];
328 }
329 else
330 {
331 return current;
332 }
333 }
334
335 return -1;
336 }
337
338 private double IntersectionX(
339 int higherSlopePeriod,
340 int lowerSlopePeriod)
341 {
342 var denominator =
343 _slope[higherSlopePeriod] -
344 _slope[lowerSlopePeriod];
345
346 if (!(denominator > 0.0) ||
347 !double.IsFinite(denominator))
348 {
349 throw new ArithmeticException(
350 "A Federgruen-Tzur candidate intersection has an invalid denominator.");
351 }
352
353 var numerator =
354 _intercept[lowerSlopePeriod] -
355 _intercept[higherSlopePeriod];
356
357 var intersection = numerator / denominator;
358
359 if (!double.IsFinite(intersection))
360 {
361 throw new ArithmeticException(
362 "A Federgruen-Tzur candidate intersection is not finite.");
363 }
364
365 return intersection;
366 }
367
368 private void Remove(int node)
369 {
370 var previous = _previous[node];
371 var next = _next[node];
372
373 if (previous >= 0)
374 {
375 _next[previous] = next;
376 }
377 else
378 {
379 _first = next;
380 }
381
382 if (next >= 0)
383 {
384 _previous[next] = previous;
385
386 if (previous < 0)
387 {
388 _startX[next] = double.NegativeInfinity;
389 }
390 }
391
392 DeleteTreeNode(node);
393
394 _previous[node] = -1;
395 _next[node] = -1;
396 }
397
398 private void DeleteTreeNode(int node)
399 {
400 int rebalanceStart;
401
402 if (_left[node] < 0)
403 {
404 rebalanceStart = _parent[node];
405 Transplant(node, _right[node]);
406 }
407 else if (_right[node] < 0)
408 {
409 rebalanceStart = _parent[node];
410 Transplant(node, _left[node]);
411 }
412 else
413 {
414 var successor = Minimum(_right[node]);
415
416 if (_parent[successor] == node)
417 {
418 Transplant(node, successor);
419
420 _left[successor] = _left[node];
421 _parent[_left[successor]] = successor;
422
423 UpdateHeight(successor);
424 rebalanceStart = successor;
425 }
426 else
427 {
428 var successorOldParent = _parent[successor];
429
430 Transplant(successor, _right[successor]);
431
432 _right[successor] = _right[node];
433 _parent[_right[successor]] = successor;
434
435 Transplant(node, successor);
436
437 _left[successor] = _left[node];
438 _parent[_left[successor]] = successor;
439
440 UpdateHeight(successor);
441 rebalanceStart = successorOldParent;
442 }
443 }
444
445 _left[node] = -1;
446 _right[node] = -1;
447 _parent[node] = -1;
448 _height[node] = 0;
449
450 Rebalance(rebalanceStart);
451 }
452
453 private int Minimum(int node)
454 {
455 var current = node;
456
457 while (_left[current] >= 0)
458 {
459 current = _left[current];
460 }
461
462 return current;
463 }
464
465 private void Transplant(int oldNode, int replacement)
466 {
467 var oldParent = _parent[oldNode];
468
469 if (oldParent < 0)
470 {
471 _root = replacement;
472 }
473 else if (_left[oldParent] == oldNode)
474 {
475 _left[oldParent] = replacement;
476 }
477 else
478 {
479 _right[oldParent] = replacement;
480 }
481
482 if (replacement >= 0)
483 {
484 _parent[replacement] = oldParent;
485 }
486 }
487
488 private void Rebalance(int node)
489 {
490 var current = node;
491
492 while (current >= 0)
493 {
494 UpdateHeight(current);
495
496 var balance = Balance(current);
497 var subtreeRoot = current;
498
499 if (balance > 1)
500 {
501 var left = _left[current];
502
503 if (Balance(left) < 0)
504 {
505 RotateLeft(left);
506 }
507
508 subtreeRoot = RotateRight(current);
509 }
510 else if (balance < -1)
511 {
512 var right = _right[current];
513
514 if (Balance(right) > 0)
515 {
516 RotateRight(right);
517 }
518
519 subtreeRoot = RotateLeft(current);
520 }
521
522 current = _parent[subtreeRoot];
523 }
524 }
525
526 private int RotateLeft(int node)
527 {
528 var pivot = _right[node];
529
530 if (pivot < 0)
531 {
532 throw new InvalidOperationException(
533 "Invalid AVL left rotation.");
534 }
535
536 var middle = _left[pivot];
537 var oldParent = _parent[node];
538
539 _parent[pivot] = oldParent;
540
541 if (oldParent < 0)
542 {
543 _root = pivot;
544 }
545 else if (_left[oldParent] == node)
546 {
547 _left[oldParent] = pivot;
548 }
549 else
550 {
551 _right[oldParent] = pivot;
552 }
553
554 _left[pivot] = node;
555 _parent[node] = pivot;
556
557 _right[node] = middle;
558
559 if (middle >= 0)
560 {
561 _parent[middle] = node;
562 }
563
564 UpdateHeight(node);
565 UpdateHeight(pivot);
566
567 return pivot;
568 }
569
570 private int RotateRight(int node)
571 {
572 var pivot = _left[node];
573
574 if (pivot < 0)
575 {
576 throw new InvalidOperationException(
577 "Invalid AVL right rotation.");
578 }
579
580 var middle = _right[pivot];
581 var oldParent = _parent[node];
582
583 _parent[pivot] = oldParent;
584
585 if (oldParent < 0)
586 {
587 _root = pivot;
588 }
589 else if (_left[oldParent] == node)
590 {
591 _left[oldParent] = pivot;
592 }
593 else
594 {
595 _right[oldParent] = pivot;
596 }
597
598 _right[pivot] = node;
599 _parent[node] = pivot;
600
601 _left[node] = middle;
602
603 if (middle >= 0)
604 {
605 _parent[middle] = node;
606 }
607
608 UpdateHeight(node);
609 UpdateHeight(pivot);
610
611 return pivot;
612 }
613
614 private int Balance(int node)
615 {
616 if (node < 0)
617 {
618 return 0;
619 }
620
621 return Height(_left[node]) - Height(_right[node]);
622 }
623
624 private int Height(int node)
625 {
626 return node < 0 ? 0 : _height[node];
627 }
628
629 private void UpdateHeight(int node)
630 {
631 if (node < 0)
632 {
633 return;
634 }
635
636 _height[node] =
637 1 + Math.Max(
638 Height(_left[node]),
639 Height(_right[node]));
640 }
641
642 private void ValidateNode(int period)
643 {
644 ThrowIfDisposed();
645
646 if ((uint)period >= (uint)_capacity ||
647 _height[period] <= 0)
648 {
649 throw new ArgumentOutOfRangeException(
650 nameof(period),
651 period,
652 "The candidate period is not active in the tree.");
653 }
654 }
655
656 private void ThrowIfDisposed()
657 {
658 ObjectDisposedException.ThrowIf(_disposed, this);
659 }
660}
bool Add(int period, double slope, double intercept)
Inserts one candidate line and removes candidates that can never be optimal for any future cumulative...
int GetBestAndDiscardPast(double cumulativeDemand)
Removes envelope candidates whose optimality interval lies entirely before the current cumulative dem...