data_structures_rmolinari 0.2.2 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3765b8df91fcc62eb885e32ff5ad4b0b4678bba6f322cb5c8282657052aed8c6
4
- data.tar.gz: 845bea3649dc51dab697927132c0fc2f62dcacf5c25e1c717b99e57819b52286
3
+ metadata.gz: 9f006234ee3b216d5607e9b10bb1958a6107ccfa0cc8c359f98383dc7fde14ee
4
+ data.tar.gz: f281ab0768e24e7c983cd046ba7b185dab8fd972fb3065fd73ff575782bf5486
5
5
  SHA512:
6
- metadata.gz: 23687561ec6ddb12369ca5e75db33ffd710295097cd0c91b72fb278fea3b11b23152867bb4aea6a4fd17b2f95184fb5433c9ff009db92a5c4bab78686ae472de
7
- data.tar.gz: d930a674f85aa0a57030ed59f2b39979c7dbb8d8f74e0a9718d44112c1efe05657e0d55773b623c3724f040054e2fedf453de30b72d5631404642081455406f9
6
+ metadata.gz: e274a97f177fad44bad20ecf24ecca1385fee3c217e7e42aac076c24377970c6444dfdbadc6fd3e1e201555177429c9f8eddaee211e463dd60f6b36e74004eec
7
+ data.tar.gz: 293fc0b2973a8d851c27f4e64177dbf7b9a25b2bb7eb9efb4b33abdb07c4e006f80f4450996ef99da7e8bb1516ca8aa89ab893258960d9127d101995906254ed
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.3.0] 2023-01-06
6
+
7
+ ### Added
8
+
9
+ - Start this file
10
+ - `Heap` can be constructed as "non-addressable"
11
+ - `update` is not possible but duplicates can be inserted and overall performance is a little better.
12
+
13
+ ### Changed
14
+
15
+ - `LogicError` gets a subclassed `InternalLogicError` for issues inside the library.
16
+ - `Shared::Pair` becomes `Shared::Point`
17
+ - this doesn't change the API of `MaxPrioritySearchTree` because of ducktyping. But client code (of which there is none) might be
18
+ using the `Pair` name.
@@ -4,7 +4,7 @@
4
4
  # The data structure provides efficient actions to merge two disjoint subsets, i.e., replace them by their union, and determine if
5
5
  # two elements are in the same subset.
6
6
  #
7
- # The elements of the set must be 0, 1, ..., n-1, where n is the size of the universe. Client code can map its data to these
7
+ # The elements of the set are 0, 1, ..., n-1, where n is the size of the universe. Client code can map its data to these
8
8
  # representatives.
9
9
  #
10
10
  # See https://en.wikipedia.org/wiki/Disjoint-set_data_structure for a good introduction.
@@ -60,7 +60,7 @@ class DataStructuresRMolinari::DisjointUnion
60
60
  end
61
61
 
62
62
  private def check_value(v)
63
- raise "Value must be given and be in (0..#{@size - 1})" unless v && v.between?(0, @size - 1)
63
+ raise DataError, "Value must be given and be in (0..#{@size - 1})" unless v && v.between?(0, @size - 1)
64
64
  end
65
65
 
66
66
  private def link(e, f)
@@ -13,7 +13,7 @@ require_relative 'shared'
13
13
  # Ruby.
14
14
  #
15
15
  # This is a generic implementation, intended to allow easy configuration for concrete instances. See the parameters to the
16
- # initializer and the defintiaons concrete realisations like MaxValSegmentTree.
16
+ # initializer and the definitions of concrete realisations like MaxValSegmentTree.
17
17
  #
18
18
  # We do O(n) work to build the internal data structure at initialization. Then we answer queries in O(log n) time.
19
19
  class DataStructuresRMolinari::GenericSegmentTree
@@ -24,14 +24,14 @@ class DataStructuresRMolinari::GenericSegmentTree
24
24
  # - For example, if we are calculating sums over subintervals, combine.call(a, b) = a + b, while if we are doing maxima we will
25
25
  # return max(a, b).
26
26
  # - Things get more complicated when we are calculating, say, the _index_ of the maximal value in a subinterval. Now it is not
27
- # enough simple to store that index at each tree node, because to combine the indices from two child nodes we need to know
27
+ # enough simply to store that index at each tree node, because to combine the indices from two child nodes we need to know
28
28
  # both the index of the maximal element in each child node's interval, but also the maximal values themselves, so we know
29
29
  # which one "wins" for the parent node. This affects the sort of work we need to do when combining and the value provided by
30
30
  # the +single_cell_array_val+ lambda.
31
31
  # @param single_cell_array_val a lambda that takes an index i and returns the value we need to store in the #build
32
32
  # operation for the subinterval i..i.
33
- # - This is often simply be the value data[i], but in some cases it will be something else. For example, when we are
34
- # calculating the index of the maximal value on each subinterval we will retern the pair [i, data[i]] here.
33
+ # - This will often simply be the value data[i], but in some cases it will be something else. For example, when we are
34
+ # calculating the index of the maximal value on each subinterval we need [i, data[i]] here.
35
35
  # - If +update_at+ is called later, this lambda must close over the underlying data in a way that captures the updated value.
36
36
  # @param size the size of the underlying data array, used in certain internal arithmetic.
37
37
  # @param identity the value to return when we are querying on an empty interval
@@ -96,7 +96,7 @@ class DataStructuresRMolinari::GenericSegmentTree
96
96
  private def update_val_at(idx, tree_idx, tree_l, tree_r)
97
97
  if tree_l == tree_r
98
98
  # We have found the spot!
99
- raise LogicError, 'tree_l == tree_r, but they do not agree with the idx holding the updated value' unless tree_l == idx
99
+ raise InternalLogicError, 'tree_l == tree_r, but they do not agree with the idx holding the updated value' unless tree_l == idx
100
100
 
101
101
  @tree[tree_idx] = @single_cell_array_val.call(tree_l)
102
102
  else
@@ -13,8 +13,8 @@ require_relative 'shared'
13
13
  # - +empty?+
14
14
  # - is the heap empty?
15
15
  # - O(1)
16
- # - +insert+
17
- # - add a new element to the heap with an associated priority
16
+ # - +insert(item, priority)+
17
+ # - add a new item to the heap with an associated priority
18
18
  # - O(log N)
19
19
  # - +top+
20
20
  # - return the lowest-priority element, which is the element at the root of the tree. In a max-heap this is the highest-priority
@@ -23,12 +23,18 @@ require_relative 'shared'
23
23
  # - +pop+
24
24
  # - removes and returns the item that would be returned by +top+
25
25
  # - O(log N)
26
- # - +update+
26
+ # - +update(item, priority)+
27
27
  # - tell the heap that the priority of a particular item has changed
28
28
  # - O(log N)
29
29
  #
30
30
  # Here N is the number of elements in the heap.
31
31
  #
32
+ # The internal requirements needed to implement +update+ have several consequences.
33
+ # - Items added to the heap must be distinct. Otherwise we would not know which occurrence to update
34
+ # - There is some bookkeeping overhead.
35
+ # If client code doesn't need to call +update+ then we can create a "non-addressable" heap that allows for the insertion of
36
+ # duplicate items and has slightly faster runtime overall. See the arguments to the initializer.
37
+ #
32
38
  # References:
33
39
  #
34
40
  # - https://en.wikipedia.org/wiki/Binary_heap
@@ -36,31 +42,31 @@ require_relative 'shared'
36
42
  # DOI 10.1007/s00224-017-9760-2
37
43
  #
38
44
  # @todo
39
- # - allow for priorities comparable only via +<=>+, like arrays
40
- # - this requires different handling for max-heaps, as we can't just negate the priorities and use min-heap logic
41
- # - relax the requirement that priorities must be comparable vai +<+ and respond to negation. Instead, allow comparison via +<=>+
42
- # and handle max-heaps differently.
43
- # - this will allow priorities to be arrays for tie-breakers and similar.
44
- # - offer a non-addressable version that doesn't support +update+
45
- # - configure through the initializer
46
- # - other operations will be a little quicker, and we can add the same item more than once. The paper by Chen et al. referenced
47
- # in the Wikipedia article for Pairing Heaps suggests that using such a priority queue for Dijkstra's algorithm and inserting
48
- # multiple copies of a key rather than updating its priority is faster in practice than other approaches that have better
49
- # theoretical performance.
45
+ # - let caller see the priority of the top element. Maybe this is useful sometimes.
50
46
  class DataStructuresRMolinari::Heap
47
+ include Shared
51
48
  include Shared::BinaryTreeArithmetic
52
49
 
50
+ # The number of items currently in the heap
53
51
  attr_reader :size
54
52
 
55
- Pair = Struct.new(:priority, :item)
53
+ # An (item, priority) pair
54
+ InternalPair = Struct.new(:item, :priority)
55
+ private_constant :InternalPair
56
56
 
57
57
  # @param max_heap when truthy, make a max-heap rather than a min-heap
58
- # @param debug when truthy, verify the heap property after each update than might violate it. This makes operations much slower.
59
- def initialize(max_heap: false, debug: false)
58
+ # @param addressable when truthy, the heap is _addressable_. This means that
59
+ # - item priorities are updatable with +update(item, p)+, and
60
+ # - items added to the heap must be distinct.
61
+ # When falsy, priorities are not updateable but items may be inserted multiple times. Operations are slightly faster because
62
+ # there is less internal bookkeeping.
63
+ # @param debug when truthy, verify the heap property after each change that might violate it. This makes operations much slower.
64
+ def initialize(max_heap: false, addressable: true, debug: false)
60
65
  @data = []
61
66
  @size = 0
62
67
  @max_heap = max_heap
63
- @index_of = {}
68
+ @addressable = addressable
69
+ @index_of = {} # used in addressable heaps
64
70
  @debug = debug
65
71
  end
66
72
 
@@ -70,26 +76,24 @@ class DataStructuresRMolinari::Heap
70
76
  end
71
77
 
72
78
  # Insert a new element into the heap with the given priority.
73
- # @param value the item to be inserted. It is an error to insert an item that is already present in the heap, though we don't
74
- # check for this.
75
- # @param priority the priority to use for new item. The values used as priorities ust be totally ordered via +<+ and, if +self+ is
76
- # a max-heap, must respond to negation +@-+ in the natural order-respecting way.
77
- # @todo
78
- # - check for duplicate
79
+ # @param value the item to be inserted.
80
+ # - If the heap is addressible (the default) it is an error to insert an item that is already present in the heap.
81
+ # @param priority the priority to use for new item. The values used as priorities must be totally ordered via +<=>+.
79
82
  def insert(value, priority)
80
- priority *= -1 if @max_heap
83
+ raise DataError, "Heap already contains #{value}" if @addressable && contains?(value)
81
84
 
82
85
  @size += 1
83
86
 
84
- d = Pair.new(priority, value)
87
+ d = InternalPair.new(value, priority)
85
88
  assign(d, @size)
86
89
 
87
90
  sift_up(@size)
88
91
  end
89
92
 
90
93
  # Return the top of the heap without removing it
91
- # @return the value with minimal (maximal for max-heaps) priority. Strictly speaking, it returns the item at the root of the
92
- # binary tree; this element has minimal priority, but there may be other elements with the same priority.
94
+ # @return a value with minimal priority (maximal for max-heaps). Strictly speaking, it returns the item at the root of the
95
+ # binary tree; this element has minimal priority, but there may be other elements with the same priority and they do not appear
96
+ # at the top of the heap in any guaranteed order.
93
97
  def top
94
98
  raise 'Heap is empty!' unless @size.positive?
95
99
 
@@ -100,12 +104,11 @@ class DataStructuresRMolinari::Heap
100
104
  # @return (see #top)
101
105
  def pop
102
106
  result = top
103
- @index_of.delete(result)
104
-
105
107
  assign(@data[@size], root)
106
108
 
107
109
  @data[@size] = nil
108
110
  @size -= 1
111
+ @index_of.delete(result) if @addressable
109
112
 
110
113
  sift_down(root) if @size.positive?
111
114
 
@@ -113,21 +116,20 @@ class DataStructuresRMolinari::Heap
113
116
  end
114
117
 
115
118
  # Update the priority of the given element and maintain the necessary heap properties.
119
+ #
116
120
  # @param element the item whose priority we are updating. It is an error to update the priority of an element not already in the
117
121
  # heap
118
122
  # @param priority the new priority
119
- #
120
- # @todo
121
- # - check that the element is in the heap
122
123
  def update(element, priority)
123
- priority *= -1 if @max_heap
124
+ raise LogicError, 'Cannot update priorities in a non-addressable heap' unless @addressable
125
+ raise DataError, "Cannot update priority for value #{element} not already in the heap" unless contains?(element)
124
126
 
125
127
  idx = @index_of[element]
126
128
  old = @data[idx].priority
127
129
  @data[idx].priority = priority
128
- if priority > old
130
+ if less_than_priority?(old, priority)
129
131
  sift_down(idx)
130
- elsif priority < old
132
+ elsif less_than_priority?(priority, old)
131
133
  sift_up(idx)
132
134
  end
133
135
 
@@ -141,7 +143,7 @@ class DataStructuresRMolinari::Heap
141
143
  x = @data[idx]
142
144
  while idx != root
143
145
  i = parent(idx)
144
- break unless x.priority < @data[i].priority
146
+ break unless less_than?(x, @data[i])
145
147
 
146
148
  assign(@data[i], idx)
147
149
  idx = i
@@ -156,9 +158,9 @@ class DataStructuresRMolinari::Heap
156
158
  x = @data[idx]
157
159
 
158
160
  while (j = left(idx)) <= @size
159
- j += 1 if j + 1 <= @size && @data[j + 1].priority < @data[j].priority
161
+ j += 1 if j + 1 <= @size && less_than?(@data[j + 1], @data[j])
160
162
 
161
- break unless @data[j].priority < x.priority
163
+ break unless less_than?(@data[j], x)
162
164
 
163
165
  assign(@data[j], idx)
164
166
  idx = j
@@ -171,7 +173,27 @@ class DataStructuresRMolinari::Heap
171
173
  # Put the pair in the given heap location
172
174
  private def assign(pair, idx)
173
175
  @data[idx] = pair
174
- @index_of[pair.item] = idx
176
+ @index_of[pair.item] = idx if @addressable
177
+ end
178
+
179
+ # Compare the priorities of two items with <=> and return truthy exactly when the result is -1.
180
+ #
181
+ # If this is a max-heap return truthy exactly when the result of <=> is 1.
182
+ #
183
+ # The arguments can also be the priorities themselves.
184
+ private def less_than?(p1, p2)
185
+ less_than_priority?(p1.priority, p2.priority)
186
+ end
187
+
188
+ # Direct comparison of priorities
189
+ private def less_than_priority?(priority1, priority2)
190
+ return (priority1 <=> priority2) == 1 if @max_heap
191
+
192
+ (priority1 <=> priority2) == -1
193
+ end
194
+
195
+ private def contains?(item)
196
+ !!@index_of[item]
175
197
  end
176
198
 
177
199
  # For debugging
@@ -180,8 +202,8 @@ class DataStructuresRMolinari::Heap
180
202
  left = left(idx)
181
203
  right = right(idx)
182
204
 
183
- raise "Heap property violated by left child of index #{idx}" if left <= @size && @data[idx].priority >= @data[left].priority
184
- raise "Heap property violated by right child of index #{idx}" if right <= @size && @data[idx].priority >= @data[right].priority
205
+ raise InternalLogicError, "Heap property violated by left child of index #{idx}" if left <= @size && less_than?(@data[left], @data[idx])
206
+ raise InternalLogicError, "Heap property violated by right child of index #{idx}" if right <= @size && less_than?(@data[right], @data[idx])
185
207
  end
186
208
  end
187
209
  end
@@ -1,13 +1,9 @@
1
1
  require 'set'
2
2
  require_relative 'shared'
3
3
 
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
4
+ # 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
5
  # questions about P.
7
6
  #
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
7
  # The data structure was introduced in 1985 by Edward McCreight. Later, De, Maheshwari, Nandy, and Smid showed how to construct a
12
8
  # PST in-place (using only O(1) extra memory), at the expense of some slightly more complicated code for the various supported
13
9
  # operations. It is their approach that we have implemented.
@@ -33,11 +29,15 @@ require_relative 'shared'
33
29
  #
34
30
  # The final operation (enumerate) takes O(m + log n) time, where m is the number of points that are enumerated.
35
31
  #
32
+ # In the current implementation no two points can share an x-value and no two points can share a y-value. This (rather severe)
33
+ # restriction can be relaxed with some more complicated code.
34
+ #
35
+ #
36
36
  # 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
37
  # MaxPST.
38
38
  #
39
39
  # References:
40
- # * E.M. McCreight, _Priority search trees_, SIAM J. Comput., 14(2):257-276, 1985. Later, De,
40
+ # * E.M. McCreight, _Priority search trees_, SIAM J. Comput., 14(2):257-276, 1985.
41
41
  # * M. De, A. Maheshwari, S. C. Nandy, M. Smid, _An In-Place Priority Search Tree_, 23rd Canadian Conference on Computational
42
42
  # Geometry, 2011
43
43
  class DataStructuresRMolinari::MaxPrioritySearchTree
@@ -113,12 +113,12 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
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 }
@@ -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
@@ -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)
@@ -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
@@ -786,7 +786,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
786
786
  p_in = right(p_in)
787
787
  left = true
788
788
  else
789
- raise LogicError, 'q_in cannot be active, by the value in the right child of p_in!' if right_in
789
+ raise InternalLogicError, 'q_in cannot be active, by the value in the right child of p_in!' if right_in
790
790
 
791
791
  p = left(p_in)
792
792
  q = right(p_in)
@@ -796,7 +796,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
796
796
  end
797
797
  elsif left_val.x <= x1
798
798
  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
799
+ raise InternalLogicError, 'q_in cannot be active, by the value in the right child of p_in!' if right_in
800
800
 
801
801
  q = right(p_in)
802
802
  p_in = left(p_in)
@@ -810,7 +810,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
810
810
  right_in = true
811
811
  end
812
812
  else
813
- raise LogicError, 'q_in cannot be active, by the value in the right child of p_in!' if right_in
813
+ raise InternalLogicError, 'q_in cannot be active, by the value in the right child of p_in!' if right_in
814
814
 
815
815
  q = left(p_in)
816
816
  deactivate_p_in.call
@@ -846,8 +846,8 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
846
846
 
847
847
  # q has two children. Cases!
848
848
  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
849
+ raise InternalLogicError, 'p_in should not be active, based on the value at left(q)' if left_in
850
+ raise InternalLogicError, 'q_in should not be active, based on the value at left(q)' if right_in
851
851
 
852
852
  left = true
853
853
  if @data[right(q)].x < x0
@@ -878,7 +878,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
878
878
 
879
879
  # Given: q' is active and satisfied x0 <= x(q') <= x1
880
880
  enumerate_right_in = lambda do
881
- raise LogicError, 'right_in should be true if we call enumerate_right_in' unless right_in
881
+ raise InternalLogicError, 'right_in should be true if we call enumerate_right_in' unless right_in
882
882
 
883
883
  if @data[q_in].y >= y0
884
884
  report.call(q_in)
@@ -910,7 +910,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
910
910
  # q' has two children
911
911
  right_val = @data[right(q_in)]
912
912
  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
913
+ raise InternalLogicError, 'p_in cannot be active, by the value in the left child of q_in' if left_in
914
914
 
915
915
  if right_val.x < x0
916
916
  p = right(q_in)
@@ -970,7 +970,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
970
970
 
971
971
  while left || left_in || right_in || right
972
972
  # 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
973
+ raise InternalLogicError, 'It should not be that q_in is active but p_in is not' if right_in && !left_in
974
974
 
975
975
  set_i = []
976
976
  set_i << :left if left
@@ -988,7 +988,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
988
988
  when :right
989
989
  enumerate_right.call
990
990
  else
991
- raise LogicError, "bad symbol #{z}"
991
+ raise InternalLogicError, "bad symbol #{z}"
992
992
  end
993
993
  end
994
994
  return result unless block_given?
@@ -1134,7 +1134,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
1134
1134
  private def verify_properties
1135
1135
  # It's a max-heap in y
1136
1136
  (2..@size).each do |node|
1137
- raise LogicError, "Heap property violated at child #{node}" unless @data[node].y < @data[parent(node)].y
1137
+ raise InternalLogicError, "Heap property violated at child #{node}" unless @data[node].y < @data[parent(node)].y
1138
1138
  end
1139
1139
 
1140
1140
  # Left subtree has x values less than all of the right subtree
@@ -1144,7 +1144,7 @@ class DataStructuresRMolinari::MaxPrioritySearchTree
1144
1144
  left_max = max_x_in_subtree(left(node))
1145
1145
  right_min = min_x_in_subtree(right(node))
1146
1146
 
1147
- raise LogicError, "Left-right property of x-values violated at #{node}" unless left_max < right_min
1147
+ raise InternalLogicError, "Left-right property of x-values violated at #{node}" unless left_max < right_min
1148
1148
  end
1149
1149
  end
1150
1150
 
@@ -2,15 +2,13 @@ require 'must_be'
2
2
 
3
3
  require_relative 'shared'
4
4
 
5
+ # THIS CLASS IS INCOMPLETE AND NOT USABLE
6
+ #
5
7
  # A priority search tree (PST) stores points in two dimensions (x,y) and can efficiently answer certain questions about the set of
6
8
  # point.
7
9
  #
8
10
  # The structure was introduced by McCreight [1].
9
11
  #
10
- # It is a binary search tree which is a max-heap by the y-coordinate, and, for a non-leaf node N storing (x, y), all the nodes in
11
- # the left subtree of N have smaller x values than any of the nodes in the right subtree of N. Note, though, that the x-value at N
12
- # has no particular property relative to the x values in its subtree. It is thus _almost_ a binary search tree in the x coordinate.
13
- #
14
12
  # See more: https://en.wikipedia.org/wiki/Priority_search_tree
15
13
  #
16
14
  # It is possible to build such a tree in place, given an array of pairs. See [2]. In a follow-up paper, [3], the authors show how to
@@ -45,7 +43,7 @@ class DataStructuresRMolinari::MinmaxPrioritySearchTree
45
43
 
46
44
  # The array of pairs is turned into a minmax PST in-place without cloning. So clone before passing it in, if you care.
47
45
  #
48
- # Each element must respond to #x and #y. Use Pair (above) if you like.
46
+ # Each element must respond to #x and #y. Use Point (above) if you like.
49
47
  def initialize(data, verify: false)
50
48
  @data = data
51
49
  @size = @data.size
@@ -75,7 +73,7 @@ class DataStructuresRMolinari::MinmaxPrioritySearchTree
75
73
  #
76
74
  # Here T(x) is the subtree rooted at x
77
75
  def leftmost_ne(x0, y0)
78
- best = Pair.new(INFINITY, INFINITY)
76
+ best = Point.new(INFINITY, INFINITY)
79
77
  p = q = root
80
78
 
81
79
  in_q = ->(pair) { pair.x >= x0 && pair.y >= y0 }
@@ -284,7 +282,7 @@ class DataStructuresRMolinari::MinmaxPrioritySearchTree
284
282
  #
285
283
  # This method returns p*
286
284
  # def highest_3_sided_up(x0, x1, y0)
287
- # best = Pair.new(INFINITY, -INFINITY)
285
+ # best = Point.new(INFINITY, -INFINITY)
288
286
 
289
287
  # in_q = lambda do |pair|
290
288
  # pair.x >= x0 && pair.x <= x1 && pair.y >= y0
@@ -407,7 +405,7 @@ class DataStructuresRMolinari::MinmaxPrioritySearchTree
407
405
  # - If Q intersect P is empty then p* = best
408
406
  #
409
407
  # Here, P is the set of points in our data structure and T_p is the subtree rooted at p
410
- best = Pair.new(INFINITY, -INFINITY)
408
+ best = Point.new(INFINITY, -INFINITY)
411
409
  p = root # root of the whole tree AND the pair stored there
412
410
 
413
411
  in_q = lambda do |pair|
@@ -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,8 @@
1
1
  require_relative 'data_structures_rmolinari/shared'
2
2
 
3
3
  module DataStructuresRMolinari
4
- Pair = Shared::Pair
4
+ # A struct responding to +.x+ and +.y+.
5
+ Point = Shared::Point
5
6
  end
6
7
 
7
8
  # These define classes inside module DataStructuresRMolinari
@@ -11,13 +12,15 @@ require_relative 'data_structures_rmolinari/heap'
11
12
  require_relative 'data_structures_rmolinari/max_priority_search_tree'
12
13
  require_relative 'data_structures_rmolinari/minmax_priority_search_tree'
13
14
 
15
+ # A namespace to hold the provided classes. We want to avoid polluting the global namespace with names like "Heap"
14
16
  module DataStructuresRMolinari
15
17
  ########################################
16
18
  # Concrete instances of Segment Tree
17
19
  #
18
20
  # @todo consider moving these into generic_segment_tree.rb
19
21
 
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.
22
+ # 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)?"
23
+ # in O(log n) time.
21
24
  class MaxValSegmentTree
22
25
  extend Forwardable
23
26
 
@@ -25,28 +28,27 @@ module DataStructuresRMolinari
25
28
  def_delegator :@structure, :update_at
26
29
 
27
30
  # @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.
31
+ # - This will usually be an Array, but it could also be a hash or a proc.
29
32
  def initialize(data)
30
33
  @structure = GenericSegmentTree.new(
31
34
  combine: ->(a, b) { [a, b].max },
32
35
  single_cell_array_val: ->(i) { data[i] },
33
36
  size: data.size,
34
- identity: -Float::INFINITY
37
+ identity: -Shared::INFINITY
35
38
  )
36
39
  end
37
40
 
38
- # The maximum value in A(i..j)
41
+ # The maximum value in A(i..j).
39
42
  #
40
43
  # The arguments must be integers in 0...(A.size)
41
- # @return the largest value in A(i..j).
42
- # - Return +nil+ if i > j
44
+ # @return the largest value in A(i..j) or -Infinity if i > j.
43
45
  def max_on(i, j)
44
46
  @structure.query_on(i, j)
45
47
  end
46
48
  end
47
49
 
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.
50
+ # 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
51
+ # subinterval A(i..j)?" in O(log n) time.
50
52
  class IndexOfMaxValSegmentTree
51
53
  extend Forwardable
52
54
 
@@ -66,7 +68,7 @@ module DataStructuresRMolinari
66
68
  # The index of the maximum value in A(i..j)
67
69
  #
68
70
  # The arguments must be integers in 0...(A.size)
69
- # @return (Integer, nil) the index of the largest value in A(i..j).
71
+ # @return (Integer, nil) the index of the largest value in A(i..j) or +nil+ if i > j.
70
72
  # - If there is more than one entry with that value, return one the indices. There is no guarantee as to which one.
71
73
  # - Return +nil+ if i > j
72
74
  def index_of_max_val_on(i, j)
metadata CHANGED
@@ -1,7 +1,7 @@
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.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rory Molinari
@@ -80,6 +80,7 @@ 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