data_structures_rmolinari 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,30 +1,27 @@
1
+ require 'must_be'
1
2
  require 'set'
2
3
  require_relative 'shared'
3
4
 
4
-
5
- # A priority search tree (PST) stores a set, P, of two-dimensional points (x,y) in a way that allows efficient answes to certain
5
+ # A priority search tree (PST) stores a set, P, of two-dimensional points (x,y) in a way that allows efficient answers to certain
6
6
  # questions about P.
7
7
  #
8
- # (In the current implementation no two points can share an x-value and no two points can share a y-value. This (rather severe)
9
- # restriction can be relaxed with some more complicated code.)
10
- #
11
8
  # The data structure was introduced in 1985 by Edward McCreight. Later, De, Maheshwari, Nandy, and Smid showed how to construct a
12
9
  # PST in-place (using only O(1) extra memory), at the expense of some slightly more complicated code for the various supported
13
10
  # operations. It is their approach that we have implemented.
14
11
  #
15
12
  # The PST structure is an implicit, balanced binary tree with the following properties:
16
- # * The tree is a _max-heap_ in the y coordinate. That is, the point at each node has a y-value less than its parent.
13
+ # * The tree is a _max-heap_ in the y coordinate. That is, the point at each node has a y-value no greater than its parent.
17
14
  # * For each node p, the x-values of all the nodes in the left subtree of p are less than the x-values of all the nodes in the right
18
15
  # subtree of p. Note that this says nothing about the x-value at the node p itself. The tree is thus _almost_ a binary search tree
19
16
  # in the x coordinate.
20
17
  #
21
18
  # Given a set of n points, we can answer the following questions quickly:
22
19
  #
23
- # - +leftmost_ne+: for x0 and y0, what is the leftmost point (x, y) in P satisfying x >= x0 and y >= y0?
24
- # - +rightmost_nw+: for x0 and y0, what is the rightmost point (x, y) in P satisfying x <= x0 and y >= y0?
25
- # - +highest_ne+: for x0 and y0, what is the highest point (x, y) in P satisfying x >= x0 and y >= y0?
26
- # - +highest_nw+: for x0 and y0, what is the highest point (x, y) in P satisfying x <= x0 and y >= y0?
27
- # - +highest_3_sided+: for x0, x1, and y0, what is the highest point (x, y) in P satisfying x >= x0, x <= x1 and y >= y0?
20
+ # - +smallest_x_in_ne+: for x0 and y0, what is the leftmost point (x, y) in P satisfying x >= x0 and y >= y0?
21
+ # - +largest_x_in_nw+: for x0 and y0, what is the rightmost point (x, y) in P satisfying x <= x0 and y >= y0?
22
+ # - +largest_y_in_ne+: for x0 and y0, what is the highest point (x, y) in P satisfying x >= x0 and y >= y0?
23
+ # - +largest_y_in_nw+: for x0 and y0, what is the highest point (x, y) in P satisfying x <= x0 and y >= y0?
24
+ # - +largest_y_in_3_sided+: for x0, x1, and y0, what is the highest point (x, y) in P satisfying x >= x0, x <= x1 and y >= y0?
28
25
  # - +enumerate_3_sided+: for x0, x1, and y0, enumerate all points in P satisfying x >= x0, x <= x1 and y >= y0.
29
26
  #
30
27
  # (Here, "leftmost/rightmost" means "minimal/maximal x", and "highest" means "maximal y".)
@@ -33,11 +30,15 @@ require_relative 'shared'
33
30
  #
34
31
  # The final operation (enumerate) takes O(m + log n) time, where m is the number of points that are enumerated.
35
32
  #
33
+ # In the current implementation no two points can share an x-value. This (rather severe) restriction can be relaxed with some more
34
+ # complicated code, but it hasn't been written yet. See issue #9.
35
+ #
36
+ #
36
37
  # There is a related data structure called the Min-max priority search tree so we have called this a "Max priority search tree", or
37
38
  # MaxPST.
38
39
  #
39
40
  # References:
40
- # * E.M. McCreight, _Priority search trees_, SIAM J. Comput., 14(2):257-276, 1985. Later, De,
41
+ # * E.M. McCreight, _Priority search trees_, SIAM J. Comput., 14(2):257-276, 1985.
41
42
  # * M. De, A. Maheshwari, S. C. Nandy, M. Smid, _An In-Place Priority Search Tree_, 23rd Canadian Conference on Computational
42
43
  # Geometry, 2011
43
44
  class DataStructuresRMolinari::MaxPrioritySearchTree
@@ -49,7 +50,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
49
50
  # @param data [Array] the set P of points presented as an array. The tree is built in the array in-place without cloning.
50
51
  # - Each element of the array must respond to +#x+ and +#y+.
51
52
  # - This is not checked explicitly but a missing method exception will be thrown when we try to call one of them.
52
- # - The +x+ values must be distinct, as must the +y+ values. We raise a +Shared::DataError+ if this isn't the case.
53
+ # - The +x+ values must be distinct. We raise a +Shared::DataError+ if this isn't the case.
53
54
  # - This is a restriction that simplifies some of the algorithm code. It can be removed as the cost of some extra work. Issue
54
55
  # #9.
55
56
  #
@@ -60,9 +61,8 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
60
61
  @size = @data.size
61
62
 
62
63
  construct_pst
63
- return unless verify
64
64
 
65
- verify_properties
65
+ verify_properties if verify
66
66
  end
67
67
 
68
68
  ########################################
@@ -74,11 +74,11 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
74
74
  # structure. Define p* as
75
75
  #
76
76
  # - (infty, -infty) if Q \intersect P is empty and
77
- # - the highest (max-x) point in Q \intersect P otherwise.
77
+ # - the highest (max-y) point in Q \intersect P otherwise, breaking ties by preferring smaller values of x
78
78
  #
79
79
  # This method returns p* in O(log n) time and O(1) extra space.
80
- def highest_ne(x0, y0)
81
- highest_in_quadrant(x0, y0, :ne)
80
+ def largest_y_in_ne(x0, y0)
81
+ largest_y_in_quadrant(x0, y0, :ne)
82
82
  end
83
83
 
84
84
  # Return the highest point in P to the "northwest" of (x0, y0).
@@ -87,17 +87,17 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
87
87
  # structure. Define p* as
88
88
  #
89
89
  # - (-infty, -infty) if Q \intersect P is empty and
90
- # - the highest (max-y) point in Q \intersect P otherwise.
90
+ # - the highest (max-y) point in Q \intersect P otherwise, breaking ties by preferring smaller values of x
91
91
  #
92
92
  # This method returns p* in O(log n) time and O(1) extra space.
93
- def highest_nw(x0, y0)
94
- highest_in_quadrant(x0, y0, :nw)
93
+ def largest_y_in_nw(x0, y0)
94
+ largest_y_in_quadrant(x0, y0, :nw)
95
95
  end
96
96
 
97
- # The basic algorithm is from De et al. section 3.1. We have generalaized it slightly to allow it to calculate both highest_ne and
98
- # highest_nw
97
+ # The basic algorithm is from De et al. section 3.1. We have generalaized it slightly to allow it to calculate both largest_y_in_ne and
98
+ # largest_y_in_nw
99
99
  #
100
- # Note that highest_ne(x0, y0) = highest_3_sided(x0, infinty, y0) so we don't really need this. But it's a bit faster than the
100
+ # Note that largest_y_in_ne(x0, y0) = largest_y_in_3_sided(x0, infinty, y0) so we don't really need this. But it's a bit faster than the
101
101
  # general case and is a simple algorithm that introduces a typical way that an algorithm interacts with the data structure.
102
102
  #
103
103
  # From the paper:
@@ -108,17 +108,17 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
108
108
  # - If Q intersect P is empty then p* = best
109
109
  #
110
110
  # Here, P is the set of points in our data structure and T_p is the subtree rooted at p
111
- private def highest_in_quadrant(x0, y0, quadrant)
111
+ private def largest_y_in_quadrant(x0, y0, quadrant)
112
112
  quadrant.must_be_in [:ne, :nw]
113
113
 
114
114
  p = root
115
115
  if quadrant == :ne
116
- best = Pair.new(INFINITY, -INFINITY)
116
+ best = Point.new(INFINITY, -INFINITY)
117
117
  preferred_child = ->(n) { right(n) }
118
118
  nonpreferred_child = ->(n) { left(n) }
119
119
  sufficient_x = ->(x) { x >= x0 }
120
120
  else
121
- best = Pair.new(-INFINITY, -INFINITY)
121
+ best = Point.new(-INFINITY, -INFINITY)
122
122
  preferred_child = ->(n) { left(n) }
123
123
  nonpreferred_child = ->(n) { right(n) }
124
124
  sufficient_x = ->(x) { x <= x0 }
@@ -135,10 +135,10 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
135
135
  #
136
136
  # takes as input a point t and does the following: if t \in Q and y(t) > y(best) then it assignes best = t
137
137
  #
138
- # Note that the paper identifies a node in the tree with its value. We need to grab the correct node.
138
+ # We break ties by preferring points with smaller x values
139
139
  update_highest = lambda do |node|
140
140
  t = @data[node]
141
- if in_q.call(t) && t.y > best.y
141
+ if in_q.call(t) && (t.y > best.y || (t.y == best.y && t.x < best.x))
142
142
  best = t
143
143
  end
144
144
  end
@@ -194,7 +194,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
194
194
  # - the leftmost (min-x) point in Q \intersect P otherwise.
195
195
  #
196
196
  # This method returns p* in O(log n) time and O(1) extra space.
197
- def leftmost_ne(x0, y0)
197
+ def smallest_x_in_ne(x0, y0)
198
198
  extremal_in_x_dimension(x0, y0, :ne)
199
199
  end
200
200
 
@@ -207,14 +207,14 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
207
207
  # - the leftmost (min-x) point in Q \intersect P otherwise.
208
208
  #
209
209
  # This method returns p* in O(log n) time and O(1) extra space.
210
- def rightmost_nw(x0, y0)
210
+ def largest_x_in_nw(x0, y0)
211
211
  extremal_in_x_dimension(x0, y0, :nw)
212
212
  end
213
213
 
214
- # A genericized version of the paper's leftmost_ne that can calculate either leftmost_ne or rightmost_nw as specifies via a
214
+ # A genericized version of the paper's smallest_x_in_ne that can calculate either smallest_x_in_ne or largest_x_in_nw as specifies via a
215
215
  # parameter.
216
216
  #
217
- # Quadrant is either :ne (which gives leftmost_ne) or :nw (which gives rightmost_nw).
217
+ # Quadrant is either :ne (which gives smallest_x_in_ne) or :nw (which gives largest_x_in_nw).
218
218
  #
219
219
  # From De et al:
220
220
  #
@@ -228,10 +228,10 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
228
228
 
229
229
  if quadrant == :ne
230
230
  sign = 1
231
- best = Pair.new(INFINITY, INFINITY)
231
+ best = Point.new(INFINITY, INFINITY)
232
232
  else
233
233
  sign = -1
234
- best = Pair.new(-INFINITY, INFINITY)
234
+ best = Point.new(-INFINITY, INFINITY)
235
235
  end
236
236
 
237
237
  p = q = root
@@ -245,7 +245,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
245
245
  # takes as input a point t and does the following: if t \in Q and x(t) < x(best) then it assignes best = t
246
246
  #
247
247
  # Note that the paper identifies a node in the tree with its value. We need to grab the correct node.
248
- update_leftmost = lambda do |node|
248
+ update_best = lambda do |node|
249
249
  t = @data[node]
250
250
  if in_q.call(t) && sign * t.x < sign * best.x
251
251
  best = t
@@ -261,13 +261,13 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
261
261
  #
262
262
  # - If x0 <= x(c1) then all subtrees have large enough x values and we look for the leftmost node in c with a large enough y
263
263
  # value. Both p and q are sent into that subtree.
264
- # - If x0 >= x(ck) the the rightmost subtree is our only hope the rightmost subtree.
264
+ # - If x0 >= x(ck) the the rightmost subtree is our only hope
265
265
  # - Otherwise, x(c1) < x0 < x(ck) and we let i be least so that x(ci) <= x0 < x(c(i+1)). Then q becomes the lefmost cj in c not
266
266
  # to the left of ci such that y(cj) >= y0, if any. p becomes ci if y(ci) >= y0 and q otherwise. If there is no such j, we put
267
267
  # q = p. This may leave both of p, q undefined which means there is no useful way forward and we return nils to signal this to
268
268
  # calling code.
269
269
  #
270
- # The same logic applies to rightmost_nw, though everything is "backwards"
270
+ # The same logic applies to largest_x_in_nw, though everything is "backwards"
271
271
  # - membership of Q depends on having a small-enough value of x, rather than a large-enough one
272
272
  # - among the ci, values towards the end of the array tend not to be in Q while values towards the start of the array tend to be
273
273
  # in Q
@@ -302,14 +302,14 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
302
302
  new_p ||= new_q # if nodes[i] is no good, send p along with q
303
303
  new_q ||= new_p # but if there is no worthwhile value for q we should send it along with p
304
304
 
305
- return [new_q, new_p] if quadrant == :nw # swap for the rightmost_nw case.
305
+ return [new_q, new_p] if quadrant == :nw # swap for the largest_x_in_nw case.
306
306
 
307
307
  [new_p, new_q]
308
308
  end
309
309
 
310
310
  until leaf?(p)
311
- update_leftmost.call(p)
312
- update_leftmost.call(q)
311
+ update_best.call(p)
312
+ update_best.call(q)
313
313
 
314
314
  if p == q
315
315
  if one_child?(p)
@@ -324,7 +324,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
324
324
  q = p # p itself is just one layer above the leaves, or is itself a leaf
325
325
  elsif one_child?(q)
326
326
  # This generic approach is not as fast as the bespoke checks described in the paper. But it is easier to maintain the code
327
- # this way and allows easy implementation of rightmost_nw
327
+ # this way and allows easy implementation of largest_x_in_nw
328
328
  p, q = determine_next_nodes.call(left(p), right(p), left(q))
329
329
  else
330
330
  p, q = determine_next_nodes.call(left(p), right(p), left(q), right(q))
@@ -332,8 +332,8 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
332
332
  break unless p # we've run out of useful nodes
333
333
  end
334
334
  end
335
- update_leftmost.call(p) if p
336
- update_leftmost.call(q) if q
335
+ update_best.call(p) if p
336
+ update_best.call(q) if q
337
337
  best
338
338
  end
339
339
 
@@ -346,10 +346,10 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
346
346
  # MaxPST. (Note that Q is empty if x1 < x0.) Define p* as
347
347
  #
348
348
  # - (infty, -infty) if Q \intersect P is empty and
349
- # - the highest (max-x) point in Q \intersect P otherwise.
349
+ # - the highest (max-y) point in Q \intersect P otherwise, breaking ties by preferring smaller x values.
350
350
  #
351
351
  # This method returns p* in O(log n) time and O(1) extra space.
352
- def highest_3_sided(x0, x1, y0)
352
+ def largest_y_in_3_sided(x0, x1, y0)
353
353
  # From the paper:
354
354
  #
355
355
  # The three real numbers x0, x1, and y0 define the three-sided range Q = [x0,x1] X [y0,∞). If Q \intersect P̸ is not \empty,
@@ -373,7 +373,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
373
373
  #
374
374
  # Sometimes we don't have a relevant node to the left or right of Q. The booleans L and R (which we call left and right) track
375
375
  # whether p and q are defined at the moment.
376
- best = Pair.new(INFINITY, -INFINITY)
376
+ best = Point.new(INFINITY, -INFINITY)
377
377
  p = q = left = right = nil
378
378
 
379
379
  x_range = (x0..x1)
@@ -389,7 +389,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
389
389
  # Note that the paper identifies a node in the tree with its value. We need to grab the correct node.
390
390
  update_highest = lambda do |node|
391
391
  t = @data[node]
392
- if in_q.call(t) && t.y > best.y
392
+ if in_q.call(t) && (t.y > best.y || (t.y == best.y && t.x < best.x))
393
393
  best = t
394
394
  end
395
395
  end
@@ -570,7 +570,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
570
570
  # My high-level understanding of the algorithm
571
571
  # --------------------------------------------
572
572
  #
573
- # We need to find all elements of Q \intersect P, so it isn't enough, as it was in highest_3_sided simply to keep track of p and
573
+ # We need to find all elements of Q \intersect P, so it isn't enough, as it was in largest_y_in_3_sided simply to keep track of p and
574
574
  # q. We need to track four nodes, p, p', q', and q which are (with a little handwaving) respectively
575
575
  #
576
576
  # - the rightmost node to the left of Q' = [x0, x1] X [-infinity, infinity],
@@ -641,7 +641,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
641
641
  end
642
642
  current = parent(current)
643
643
  else
644
- raise LogicError, "Explore(t) state is somehow #{state} rather than 0, 1, or 2."
644
+ raise InternalLogicError, "Explore(t) state is somehow #{state} rather than 0, 1, or 2."
645
645
  end
646
646
  end
647
647
  end
@@ -692,8 +692,6 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
692
692
  # The four key helpers described in the paper
693
693
 
694
694
  # Handle the next step of the subtree at p
695
- #
696
- # I need to go through this with paper, pencil, and some diagrams.
697
695
  enumerate_left = lambda do
698
696
  if leaf?(p)
699
697
  left = false
@@ -786,7 +784,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
786
784
  p_in = right(p_in)
787
785
  left = true
788
786
  else
789
- raise LogicError, 'q_in cannot be active, by the value in the right child of p_in!' if right_in
787
+ raise InternalLogicError, 'q_in cannot be active, by the value in the right child of p_in!' if right_in
790
788
 
791
789
  p = left(p_in)
792
790
  q = right(p_in)
@@ -796,7 +794,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
796
794
  end
797
795
  elsif left_val.x <= x1
798
796
  if right_val.x > x1
799
- raise LogicError, 'q_in cannot be active, by the value in the right child of p_in!' if right_in
797
+ raise InternalLogicError, 'q_in cannot be active, by the value in the right child of p_in!' if right_in
800
798
 
801
799
  q = right(p_in)
802
800
  p_in = left(p_in)
@@ -810,7 +808,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
810
808
  right_in = true
811
809
  end
812
810
  else
813
- raise LogicError, 'q_in cannot be active, by the value in the right child of p_in!' if right_in
811
+ raise InternalLogicError, 'q_in cannot be active, by the value in the right child of p_in!' if right_in
814
812
 
815
813
  q = left(p_in)
816
814
  deactivate_p_in.call
@@ -846,8 +844,8 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
846
844
 
847
845
  # q has two children. Cases!
848
846
  if @data[left(q)].x < x0
849
- raise LogicError, 'p_in should not be active, based on the value at left(q)' if left_in
850
- raise LogicError, 'q_in should not be active, based on the value at left(q)' if right_in
847
+ raise InternalLogicError, 'p_in should not be active, based on the value at left(q)' if left_in
848
+ raise InternalLogicError, 'q_in should not be active, based on the value at left(q)' if right_in
851
849
 
852
850
  left = true
853
851
  if @data[right(q)].x < x0
@@ -878,7 +876,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
878
876
 
879
877
  # Given: q' is active and satisfied x0 <= x(q') <= x1
880
878
  enumerate_right_in = lambda do
881
- raise LogicError, 'right_in should be true if we call enumerate_right_in' unless right_in
879
+ raise InternalLogicError, 'right_in should be true if we call enumerate_right_in' unless right_in
882
880
 
883
881
  if @data[q_in].y >= y0
884
882
  report.call(q_in)
@@ -910,7 +908,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
910
908
  # q' has two children
911
909
  right_val = @data[right(q_in)]
912
910
  if left_val.x < x0
913
- raise LogicError, 'p_in cannot be active, by the value in the left child of q_in' if left_in
911
+ raise InternalLogicError, 'p_in cannot be active, by the value in the left child of q_in' if left_in
914
912
 
915
913
  if right_val.x < x0
916
914
  p = right(q_in)
@@ -970,7 +968,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
970
968
 
971
969
  while left || left_in || right_in || right
972
970
  # byebug if $do_it
973
- raise LogicError, 'It should not be that q_in is active but p_in is not' if right_in && !left_in
971
+ raise InternalLogicError, 'It should not be that q_in is active but p_in is not' if right_in && !left_in
974
972
 
975
973
  set_i = []
976
974
  set_i << :left if left
@@ -988,7 +986,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
988
986
  when :right
989
987
  enumerate_right.call
990
988
  else
991
- raise LogicError, "bad symbol #{z}"
989
+ raise InternalLogicError, "bad symbol #{z}"
992
990
  end
993
991
  end
994
992
  return result unless block_given?
@@ -999,13 +997,14 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
999
997
 
1000
998
  private def construct_pst
1001
999
  raise DataError, 'Duplicate x values are not supported' if contains_duplicates?(@data, by: :x)
1002
- raise DataError, 'Duplicate y values are not supported' if contains_duplicates?(@data, by: :y)
1003
1000
 
1004
- # We follow the algorithm in the paper by De, Maheshwari et al.
1001
+ # We follow the algorithm in the paper by De, Maheshwari et al, which takes O(n log^2 n) time. Their follow-up paper that
1002
+ # defines the Min-max PST, describes how to do the construction in O(n log n) time, but it is more complex and probably not
1003
+ # worth the trouble of both a bespoke heapsort the special sorting algorithm of Katajainen and Pasanen.
1005
1004
 
1006
- # Since we are building an implicit binary tree, things are simpler if the array is 1-based. This probably requires a malloc and
1007
- # data copy, which isn't great, but it's in the C layer so cheap compared to the O(n log^2 n) work we need to do for
1008
- # construction. In fact, we are probably doing O(n^2) work because of all the calls to #index_with_largest_y_in.
1005
+ # Since we are building an implicit binary tree, things are simpler if the array is 1-based. This requires a malloc (perhaps)
1006
+ # and memcpy (for sure), which isn't great, but it's in the C layer so cheap compared to the O(n log^2 n) work we need to do for
1007
+ # construction.
1009
1008
  @data.unshift nil
1010
1009
 
1011
1010
  h = Math.log2(@size).floor
@@ -1052,63 +1051,14 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
1052
1051
  end
1053
1052
  end
1054
1053
 
1055
- ########################################
1056
- # Tree arithmetic
1057
-
1058
- # # First element and root of the tree structure
1059
- # private def root
1060
- # 1
1061
- # end
1062
-
1063
- # # Indexing is from 1
1064
- # private def parent(i)
1065
- # i >> 1
1066
- # end
1067
-
1068
- # private def left(i)
1069
- # i << 1
1070
- # end
1071
-
1072
- # private def right(i)
1073
- # 1 + (i << 1)
1074
- # end
1075
-
1076
- # private def level(i)
1077
- # l = 0
1078
- # while i > root
1079
- # i >>= 1
1080
- # l += 1
1081
- # end
1082
- # l
1083
- # end
1084
-
1085
- # # i has no children
1086
- # private def leaf?(i)
1087
- # i > @last_non_leaf
1088
- # end
1089
-
1090
- # # i has exactly one child (the left)
1091
- # private def one_child?(i)
1092
- # i == @parent_of_one_child
1093
- # end
1094
-
1095
- # # i has two children
1096
- # private def two_children?(i)
1097
- # i <= @last_parent_of_two_children
1098
- # end
1099
-
1100
- # # i is the left child of its parent.
1101
- # private def left_child?(i)
1102
- # (i & 1).zero?
1103
- # end
1104
-
1105
1054
  private def swap(index1, index2)
1106
1055
  return if index1 == index2
1107
1056
 
1108
1057
  @data[index1], @data[index2] = @data[index2], @data[index1]
1109
1058
  end
1110
1059
 
1111
- # The index in @data[l..r] having the largest value for y
1060
+ # The index in @data[l..r] having the largest value for y, breaking ties with the smaller x value. Since we are already sorted by
1061
+ # x we don't actually need to check this.
1112
1062
  private def index_with_largest_y_in(l, r)
1113
1063
  return nil if r < l
1114
1064
 
@@ -1134,7 +1084,8 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
1134
1084
  private def verify_properties
1135
1085
  # It's a max-heap in y
1136
1086
  (2..@size).each do |node|
1137
- raise LogicError, "Heap property violated at child #{node}" unless @data[node].y < @data[parent(node)].y
1087
+ byebug unless @data[node].y <= @data[parent(node)].y
1088
+ raise InternalLogicError, "Heap property violated at child #{node}" unless @data[node].y <= @data[parent(node)].y
1138
1089
  end
1139
1090
 
1140
1091
  # Left subtree has x values less than all of the right subtree
@@ -1144,7 +1095,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
1144
1095
  left_max = max_x_in_subtree(left(node))
1145
1096
  right_min = min_x_in_subtree(right(node))
1146
1097
 
1147
- raise LogicError, "Left-right property of x-values violated at #{node}" unless left_max < right_min
1098
+ raise InternalLogicError, "Left-right property of x-values violated at #{node}" unless left_max < right_min
1148
1099
  end
1149
1100
  end
1150
1101
 
@@ -1,11 +1,19 @@
1
1
  # Some odds and ends shared by other classes
2
2
  module Shared
3
+ # Infinity without having to put a +Float::+ prefix every time
3
4
  INFINITY = Float::INFINITY
4
5
 
5
- Pair = Struct.new(:x, :y)
6
+ # An (x, y) coordinate pair.
7
+ Point = Struct.new(:x, :y)
6
8
 
7
9
  # @private
10
+
11
+ # Used for errors related to logic errors in client code
8
12
  class LogicError < StandardError; end
13
+ # Used for errors related to logic errors in library code
14
+ class InternalLogicError < LogicError; end
15
+
16
+ # Used for errors related to data, such as duplicated elements where they must be distinct.
9
17
  class DataError < StandardError; end
10
18
 
11
19
  # @private
@@ -1,7 +1,10 @@
1
+ require 'forwardable'
2
+
1
3
  require_relative 'data_structures_rmolinari/shared'
2
4
 
3
5
  module DataStructuresRMolinari
4
- Pair = Shared::Pair
6
+ # A struct responding to +.x+ and +.y+.
7
+ Point = Shared::Point
5
8
  end
6
9
 
7
10
  # These define classes inside module DataStructuresRMolinari
@@ -9,15 +12,16 @@ require_relative 'data_structures_rmolinari/disjoint_union'
9
12
  require_relative 'data_structures_rmolinari/generic_segment_tree'
10
13
  require_relative 'data_structures_rmolinari/heap'
11
14
  require_relative 'data_structures_rmolinari/max_priority_search_tree'
12
- require_relative 'data_structures_rmolinari/minmax_priority_search_tree'
13
15
 
16
+ # A namespace to hold the provided classes. We want to avoid polluting the global namespace with names like "Heap"
14
17
  module DataStructuresRMolinari
15
18
  ########################################
16
19
  # Concrete instances of Segment Tree
17
20
  #
18
- # @todo consider moving these into generic_segment_tree.rb
21
+ # @todo consider moving these into generic_segment_tree.rb and renaming that file
19
22
 
20
- # Takes an array A(0...n) and tells us what the maximum value is on a subinterval A(i..j) in O(log n) time.
23
+ # A segment tree that for an array A(0...n) answers questions of the form "what is the maximum value in the subinterval A(i..j)?"
24
+ # in O(log n) time.
21
25
  class MaxValSegmentTree
22
26
  extend Forwardable
23
27
 
@@ -25,28 +29,27 @@ module DataStructuresRMolinari
25
29
  def_delegator :@structure, :update_at
26
30
 
27
31
  # @param data an object that contains values at integer indices based at 0, via +data[i]+.
28
- # - The usual use case will be an Array, but it could also be a hash or a proc of some sort.
32
+ # - This will usually be an Array, but it could also be a hash or a proc.
29
33
  def initialize(data)
30
- @structure = GenericSegmentTree.new(
34
+ @structure = SegmentTreeTemplate.new(
31
35
  combine: ->(a, b) { [a, b].max },
32
36
  single_cell_array_val: ->(i) { data[i] },
33
37
  size: data.size,
34
- identity: -Float::INFINITY
38
+ identity: -Shared::INFINITY
35
39
  )
36
40
  end
37
41
 
38
- # The maximum value in A(i..j)
42
+ # The maximum value in A(i..j).
39
43
  #
40
44
  # The arguments must be integers in 0...(A.size)
41
- # @return the largest value in A(i..j).
42
- # - Return +nil+ if i > j
45
+ # @return the largest value in A(i..j) or -Infinity if i > j.
43
46
  def max_on(i, j)
44
47
  @structure.query_on(i, j)
45
48
  end
46
49
  end
47
50
 
48
- # A segment tree that for an array A(0...n) efficiently answers questions of the form "what is the index of the maximal value in
49
- # a subinterval A(i..j) in O(log n) time.
51
+ # A segment tree that for an array A(0...n) answers questions of the form "what is the index of the maximal value in the
52
+ # subinterval A(i..j)?" in O(log n) time.
50
53
  class IndexOfMaxValSegmentTree
51
54
  extend Forwardable
52
55
 
@@ -55,7 +58,7 @@ module DataStructuresRMolinari
55
58
 
56
59
  # @param (see MaxValSegmentTree#initialize)
57
60
  def initialize(data)
58
- @structure = GenericSegmentTree.new(
61
+ @structure = SegmentTreeTemplate.new(
59
62
  combine: ->(p1, p2) { p1[1] >= p2[1] ? p1 : p2 },
60
63
  single_cell_array_val: ->(i) { [i, data[i]] },
61
64
  size: data.size,
@@ -66,7 +69,7 @@ module DataStructuresRMolinari
66
69
  # The index of the maximum value in A(i..j)
67
70
  #
68
71
  # The arguments must be integers in 0...(A.size)
69
- # @return (Integer, nil) the index of the largest value in A(i..j).
72
+ # @return (Integer, nil) the index of the largest value in A(i..j) or +nil+ if i > j.
70
73
  # - If there is more than one entry with that value, return one the indices. There is no guarantee as to which one.
71
74
  # - Return +nil+ if i > j
72
75
  def index_of_max_val_on(i, j)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: data_structures_rmolinari
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rory Molinari
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-06 00:00:00.000000000 Z
11
+ date: 2023-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: must_be
@@ -80,12 +80,12 @@ executables: []
80
80
  extensions: []
81
81
  extra_rdoc_files: []
82
82
  files:
83
+ - CHANGELOG.md
83
84
  - lib/data_structures_rmolinari.rb
84
85
  - lib/data_structures_rmolinari/disjoint_union.rb
85
86
  - lib/data_structures_rmolinari/generic_segment_tree.rb
86
87
  - lib/data_structures_rmolinari/heap.rb
87
88
  - lib/data_structures_rmolinari/max_priority_search_tree.rb
88
- - lib/data_structures_rmolinari/minmax_priority_search_tree.rb
89
89
  - lib/data_structures_rmolinari/shared.rb
90
90
  homepage: https://github.com/rmolinari/data_structures
91
91
  licenses: