nclist-cpp
C++ implementation of nested containment lists
Loading...
Searching...
No Matches
build.hpp
Go to the documentation of this file.
1#ifndef NCLIST_BUILD_HPP
2#define NCLIST_BUILD_HPP
3
4#include <stdexcept>
5#include <vector>
6#include <cstddef>
7#include <algorithm>
8#include <numeric>
9#include <limits>
10#include <type_traits>
11
17namespace nclist {
18
27template<typename Index_, typename Position_>
28struct Nclist {
32 // Sequence of `nodes` that are children of the root node, i.e., `nodes[i]` is a child for `i` in `[0, root_children)`.
33 Index_ root_children = 0;
34
35 struct Node {
36 Node() = default;
37 Node(Index_ id) : id(id) {}
38
39 // Index of the subject interval in the user-supplied arrays.
40 Index_ id = 0;
41
42 // Sequence of `nodes` that are children of this node, i.e., `nodes[i]` is a child for `i` in `[children_start, children_end)`.
43 // Note that length of `nodes` is no greater than the number of intervals, so we can use Index_ as the indexing type.
44 Index_ children_start = 0;
45 Index_ children_end = 0;
46
47 // Sequence of `duplicates` containing indices of subject intervals that are duplicates of `id`,
48 // i.e., `duplicates[i]` is a duplicate interval for `i` in `[duplicates_start, duplicates_end)`.
49 // Note that length of `duplicates` is no greater than the number of intervals, so we can use Index_ as the indexing type.
50 Index_ duplicates_start = 0;
51 Index_ duplicates_end = 0;
52 };
53
54 std::vector<Node> nodes;
55
56 // These are the start and end positions corresponding to Node::id,
57 // i.e., `starts[i]` is equal to `subject_starts[nodes[i].id]` where `subject_starts` is the `starts` in `build()`.
58 // We put them in a separate vector for better cache locality in binary searches.
59 std::vector<Position_> starts, ends;
60
61 // Concatenations of the individual `duplicates` vectors, to reduce fragmentation.
62 std::vector<Index_> duplicates;
66};
67
73template<class Array_>
74using ArrayElement = typename std::remove_const<typename std::remove_reference<decltype(std::declval<Array_>()[0])>::type>::type;
75
79template<class Container_, typename Size_>
80void safe_resize(Container_& container, Size_ size) {
81 typedef decltype(container.size()) Csize;
82 constexpr Csize max_csize = std::numeric_limits<Csize>::max();
83 constexpr Size_ max_size = std::numeric_limits<Size_>::max();
84 if constexpr(static_cast<typename std::make_unsigned<Size_>::type>(max_size) > static_cast<typename std::make_unsigned<Csize>::type>(max_csize)) {
85 if (static_cast<typename std::make_unsigned<Size_>::type>(size) > static_cast<typename std::make_unsigned<Csize>::type>(max_csize)) {
86 throw std::runtime_error("failed to resize container to specified size");
87 }
88 }
89 container.resize(size);
90}
91
92template<typename Iterator_, typename Size_>
93void check_safe_ptrdiff(Size_ size) {
94 typedef decltype(std::declval<Iterator_>() - std::declval<Iterator_>()) Diff;
95 constexpr Diff max_diff = std::numeric_limits<Diff>::max();
96 constexpr Size_ max_size = std::numeric_limits<Size_>::max();
97 if constexpr(static_cast<typename std::make_unsigned<Size_>::type>(max_size) > static_cast<typename std::make_unsigned<Diff>::type>(max_diff)) {
98 if (static_cast<typename std::make_unsigned<Size_>::type>(size) > static_cast<typename std::make_unsigned<Diff>::type>(max_diff)) {
99 throw std::runtime_error("potential integer overflow from iterator subtraction");
100 }
101 }
102}
103
104template<typename Index_, class StartArray_, class EndArray_>
105Nclist<Index_, ArrayElement<StartArray_> > build_internal(std::vector<Index_> of_interest, const StartArray_& starts, const EndArray_& ends) {
106 typedef ArrayElement<StartArray_> Position;
107 static_assert(std::is_same<Position, ArrayElement<EndArray_> >::value);
108
109 // We want to sort by increasing start but DECREASING end, so that the children sort after their parents.
110 auto cmp = [&](Index_ l, Index_ r) -> bool {
111 if (starts[l] == starts[r]) {
112 return ends[l] > ends[r];
113 } else {
114 return starts[l] < starts[r];
115 }
116 };
117 if (!std::is_sorted(of_interest.begin(), of_interest.end(), cmp)) {
118 std::sort(of_interest.begin(), of_interest.end(), cmp);
119 }
120
121 auto num_intervals = of_interest.size();
122 typedef typename Nclist<Index_, Position>::Node WorkingNode;
123 std::vector<WorkingNode> working_list;
124 working_list.reserve(num_intervals);
125
126 // This section deserves some explanation.
127 // For each node in the list, we need to track its children.
128 // This is most easily done by allocating a vector per node, but that is very time-consuming when we have millions of nodes.
129 //
130 // Instead, we recognize that the number of child indices is no greater than the number of intervals (as each interval must only have one parent).
131 // We allocate a 'working_children' vector of length equal to the number of intervals.
132 // The left side of the vector contains the already-processed child indices, while the right side contains the currently-processed indices.
133 // The aim is to store all children in a single memory allocation that can be easily freed.
134 //
135 // At each level, we add child indices to the right side of the vector, moving in torwards the center.
136 // Once we move past a level (i.e., we discard it from 'history'), we shift its indices from the right to the left, as no more children will be added.
137 // We will always have space to perform the left shift as we know the upper bound on the number of child indices.
138 // This move also exposes the indices of that level's parent on the right of the vector, allowing for further addition of new children to that parent.
139 //
140 // The simplest approach is to left shift the children of each level separately.
141 // However, if we are discarding multiple levels at once, we can accumulate their associated contiguous slices of the 'working_children' vector.
142 // This allows us to perform a single left shift, eliminating overhead from repeated function calls.
143 //
144 // A quirk of this approach is that, because we add on the right towards the center, the children are stored in reverse order of addition.
145 // This requires some later work to undo this, to report the correct order for binary search.
146 //
147 // We use the same approach for duplicates.
148 std::vector<Index_> working_children;
149 safe_resize(working_children, num_intervals);
150 Index_ children_used = 0, children_tmp_boundary = num_intervals;
151 std::vector<Index_> working_duplicates;
152 safe_resize(working_duplicates, num_intervals);
153 Index_ duplicates_used = 0, duplicates_tmp_boundary = num_intervals;
154
155 struct Level {
156 Level() = default;
157 Level(Index_ offset, Position end) : offset(offset), end(end) {}
158 Index_ offset; // offset into `working_list`
159 Position end; // storing the end coordinate for better cache locality.
160 Index_ num_children = 0;
161 Index_ num_duplicates = 0;
162 };
163 std::vector<Level> levels(1);
164
165 Index_ num_children_to_copy = 0;
166 Index_ num_duplicates_to_copy = 0;
167 auto process_level = [&](const Level& curlevel) -> void {
168 auto& original_node = working_list[curlevel.offset];
169
170 if (curlevel.num_children) {
171 original_node.children_start = children_used + num_children_to_copy;
172 num_children_to_copy += curlevel.num_children;
173 original_node.children_end = children_used + num_children_to_copy;
174 }
175
176 if (curlevel.num_duplicates) {
177 original_node.duplicates_start = duplicates_used + num_duplicates_to_copy;
178 num_duplicates_to_copy += curlevel.num_duplicates;
179 original_node.duplicates_end = duplicates_used + num_duplicates_to_copy;
180 }
181 };
182
183 auto left_shift_indices = [&]() -> void {
184 if (num_children_to_copy) {
185 if (children_used != children_tmp_boundary) { // protect the copy, though this should only be relevant at the end of the traversal.
186 std::copy_n(working_children.begin() + children_tmp_boundary, num_children_to_copy, working_children.begin() + children_used);
187 }
188 children_used += num_children_to_copy;
189 children_tmp_boundary += num_children_to_copy;
190 }
191
192 if (num_duplicates_to_copy) {
193 if (duplicates_used != duplicates_tmp_boundary) { // protect the copy, though this should only be relevant at the end of the traversal.
194 std::copy_n(working_duplicates.begin() + duplicates_tmp_boundary, num_duplicates_to_copy, working_duplicates.begin() + duplicates_used);
195 }
196 duplicates_used += num_duplicates_to_copy;
197 duplicates_tmp_boundary += num_duplicates_to_copy;
198 }
199 };
200
201 Index_ last_id = 0;
202 for (const auto& curid : of_interest) {
203 auto curend = ends[curid];
204
205 if (levels.size() > 1) { // i.e., We've processed our first interval.
206 auto last_end = levels.back().end;
207 if (last_end < curend) { // If we're no longer nested within the previous interval, we need to back up to the root until we are nested.
208 num_children_to_copy = 0;
209 num_duplicates_to_copy = 0;
210 do {
211 const auto& curlevel = levels.back();
212 process_level(curlevel);
213 levels.pop_back();
214 } while (levels.size() > 1 && levels.back().end < curend);
215 left_shift_indices();
216
217 } else if (last_end == curend) { // Special handling of duplicate intervals.
218 if (starts[curid] == starts[last_id]) { // Only accessing 'starts' if we're forced to.
219 ++(levels.back().num_duplicates);
220 --duplicates_tmp_boundary;
221 working_duplicates[duplicates_tmp_boundary] = curid;
222 continue;
223 }
224 }
225 }
226
227 auto used = working_list.size();
228 working_list.emplace_back(curid);
229 ++(levels.back().num_children);
230 --children_tmp_boundary;
231 working_children[children_tmp_boundary] = used;
232 levels.emplace_back(used, curend);
233
234 last_id = curid;
235 }
236
237 num_children_to_copy = 0;
238 num_duplicates_to_copy = 0;
239 while (levels.size() > 1) { // processing all remaining levels except for the root node, which we'll handle separately.
240 const auto& curlevel = levels.back();
241 process_level(curlevel);
242 levels.pop_back();
243 }
244 left_shift_indices();
245
246 of_interest.clear(); // freeing up some memory for more allocations in the next section.
247 of_interest.shrink_to_fit();
248
249 // We convert the `working_list` into the output format where the children's start/end coordinates are laid out contiguously.
250 // This should make for an easier binary search (allowing us to use std::lower_bound and friends) and improve cache locality.
251 // We do this by traversing the list in a depth-first manner and adding all children of each node to the output vectors.
252 Nclist<Index_, Position> output;
253 safe_resize(output.nodes, working_list.size());
254 safe_resize(output.starts, working_list.size());
255 safe_resize(output.ends, working_list.size());
256 output.duplicates.reserve(duplicates_used);
257
258 // We compute iterator differences to obtain an index after std::lower_bound and friends.
259 // We want to ensure that the difference fits in the difference type without overflow.
260 // This can be guaranteed by checking that the difference type is large enough to hold the vector's full length.
261 // We only need to check output.starts as output.ends is the same type so will have the same difference type.
262 // We also cast to Index_ as this gives us a chance to avoid the check at compile time,
263 // given that working_list.size() <= of_interest.size() == num_intervals/num_subset.
264 check_safe_ptrdiff<decltype(output.starts.begin())>(static_cast<Index_>(working_list.size()));
265
266 struct Level2 {
267 Level2() = default;
268 Level2(Index_ working_at, Index_ working_start, Index_ output_offset) : working_at(working_at), working_start(working_start), output_offset(output_offset) {}
269 Index_ working_at;
270 Index_ working_start;
271 Index_ output_offset;
272 };
273 std::vector<Level2> history;
274
275 output.root_children = levels.front().num_children;
276 Index_ output_children_used = output.root_children;
277 history.emplace_back(working_children.size(), children_tmp_boundary, 0);
278
279 while (1) {
280 auto& current_state = history.back();
281 if (current_state.working_at == current_state.working_start) {
282 history.pop_back();
283 if (history.empty()) {
284 break;
285 } else {
286 continue;
287 }
288 }
289
290 // Remember that we inserted children and duplicates in reverse order into their working vectors.
291 // So when we copy, we do so in reverse order to cancel out the reversal, hence the decrement here.
292 --(current_state.working_at);
293 auto current_working_at = current_state.working_at;
294 auto current_output_offset = current_state.output_offset;
295 ++(current_state.output_offset); // do all modifications to current_state before the emplace_back(), otherwise the history might get reallocated and the reference would be dangling.
296
297 auto child = working_children[current_working_at];
298 auto& current = output.nodes[current_output_offset];
299 current = working_list[child];
300
301 // Starts and ends are guaranteed to be sorted for all children of a given node (after we cancel out the reversal).
302 // Obviously we already sorted in order of increasing starts, and interval indices were added to each node's children in that order.
303 // For the ends, this is less obvious but any end that is equal to or less than the previous end should be a child of that previous interval and should not show up here.
304 output.starts[current_output_offset] = starts[current.id];
305 output.ends[current_output_offset] = ends[current.id];
306
307 auto duplicates_old_start = current.duplicates_start, duplicates_old_end = current.duplicates_end;
308 if (duplicates_old_start != duplicates_old_end) {
309 current.duplicates_start = output.duplicates.size();
310 output.duplicates.insert(
311 output.duplicates.end(),
312 working_duplicates.rbegin() + (num_intervals - duplicates_old_end),
313 working_duplicates.rbegin() + (num_intervals - duplicates_old_start)
314 );
315 current.duplicates_end = output.duplicates.size();
316 }
317
318 auto children_old_start = current.children_start, children_old_end = current.children_end;
319 if (children_old_start != children_old_end) {
320 current.children_start = output_children_used;
321 output_children_used += children_old_end - children_old_start;
322 current.children_end = output_children_used;
323 history.emplace_back(children_old_end, children_old_start, current.children_start);
324 }
325 }
326
327 return output;
328}
349template<typename Index_, class StartArray_, class EndArray_>
350Nclist<Index_, ArrayElement<StartArray_> > build_custom(Index_ num_subset, const Index_* subset, const StartArray_& starts, const EndArray_& ends) {
351 std::vector<Index_> of_interest(subset, subset + num_subset);
352 return build_internal(std::move(of_interest), starts, ends);
353}
354
371template<typename Index_, class StartArray_, class EndArray_>
372Nclist<Index_, ArrayElement<StartArray_> > build_custom(Index_ num_intervals, const StartArray_& starts, const EndArray_& ends) {
373 std::vector<Index_> of_interest(num_intervals);
374 std::iota(of_interest.begin(), of_interest.end(), static_cast<Index_>(0));
375 return build_internal(std::move(of_interest), starts, ends);
376}
377
392template<typename Index_, typename Position_>
393Nclist<Index_, Position_> build(Index_ num_subset, const Index_* subset, const Position_* starts, const Position_* ends) {
394 return build_custom<Index_>(num_subset, subset, starts, ends);
395}
396
409template<typename Index_, typename Position_>
410Nclist<Index_, Position_> build(Index_ num_intervals, const Position_* starts, const Position_* ends) {
411 return build_custom<Index_>(num_intervals, starts, ends);
412}
413
414}
415
416#endif
Header-only library for nested containment lists.
Definition build.hpp:17
Nclist< Index_, ArrayElement< StartArray_ > > build_custom(Index_ num_subset, const Index_ *subset, const StartArray_ &starts, const EndArray_ &ends)
Definition build.hpp:350
Nclist< Index_, Position_ > build(Index_ num_subset, const Index_ *subset, const Position_ *starts, const Position_ *ends)
Definition build.hpp:393
typename std::remove_const< typename std::remove_reference< decltype(std::declval< Array_ >()[0])>::type >::type ArrayElement
Definition build.hpp:74
Pre-built nested containment list.
Definition build.hpp:28